mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 22:55:48 +01:00
[v3 alpha] windows tray minor refactor (#4653)
* remove systray add retry loop * correct setTooltip truncation * track Windows tray icon ownership to avoid destroying shared handles * fix incorrect warning call * stop leaking Windows tray theme listener after destroy * fix default app icon loading * fix incorrect truncation * harden system tray flow * implement windows tray show/hide * improved readability * updateIcon call path * improve error handling * systray clock example * added changelog --------- Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
This commit is contained in:
parent
8ac5984dfd
commit
d58d4ba758
6 changed files with 336 additions and 136 deletions
|
|
@ -17,12 +17,18 @@ After processing, the content will be moved to the main changelog and this file
|
|||
|
||||
## Added
|
||||
<!-- New features, capabilities, or enhancements -->
|
||||
- Add `systray-clock` example showing a headless tray with live tooltip updates (#4653).
|
||||
|
||||
## Changed
|
||||
<!-- Changes in existing functionality -->
|
||||
- Windows trays now honor `SystemTray.Show()`/`Hide()` by toggling `NIS_HIDDEN`, so apps can truly disappear and return (#4653).
|
||||
- Tray registration reuses resolved icons, sets `NOTIFYICON_VERSION_4` once, and enables `NIF_SHOWTIP` so tooltips recover after Explorer restarts (#4653).
|
||||
|
||||
## Fixed
|
||||
<!-- Bug fixes -->
|
||||
- Track `HICON` ownership so only user-created handles are destroyed, preventing Explorer recycling crashes (#4653).
|
||||
- Release the Windows system-theme listener and retained tray icons during destroy to stop leaking goroutines and device contexts (#4653).
|
||||
- Truncate tray tooltips at 127 UTF-16 units to avoid corrupting surrogate pairs and multi-byte glyphs (#4653).
|
||||
|
||||
## Deprecated
|
||||
<!-- Soon-to-be removed features -->
|
||||
|
|
|
|||
59
v3/examples/systray-clock/main.go
Normal file
59
v3/examples/systray-clock/main.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/icons"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := application.New(application.Options{
|
||||
Name: "Systray Clock",
|
||||
Description: "System tray clock with live tooltip updates",
|
||||
Assets: application.AlphaAssets,
|
||||
Mac: application.MacOptions{
|
||||
ActivationPolicy: application.ActivationPolicyAccessory,
|
||||
},
|
||||
Windows: application.WindowsOptions{
|
||||
DisableQuitOnLastWindowClosed: true,
|
||||
},
|
||||
})
|
||||
|
||||
systemTray := app.SystemTray.New()
|
||||
|
||||
// Use the template icon on macOS so the clock respects light/dark modes.
|
||||
if runtime.GOOS == "darwin" {
|
||||
systemTray.SetTemplateIcon(icons.SystrayMacTemplate)
|
||||
}
|
||||
|
||||
menu := app.NewMenu()
|
||||
menu.Add("Quit").OnClick(func(ctx *application.Context) {
|
||||
app.Quit()
|
||||
})
|
||||
systemTray.SetMenu(menu)
|
||||
|
||||
updateTooltip := func() {
|
||||
systemTray.SetTooltip(time.Now().Format("15:04:05"))
|
||||
}
|
||||
updateTooltip()
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
updateTooltip()
|
||||
case <-app.Context().Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := app.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/go-webview2/webviewloader"
|
||||
|
||||
"github.com/wailsapp/wails/v3/internal/operatingsystem"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
|
|
@ -124,9 +125,9 @@ func (m *windowsApp) setIcon(_ []byte) {
|
|||
}
|
||||
|
||||
func (m *windowsApp) name() string {
|
||||
//appName := C.getAppName()
|
||||
//defer C.free(unsafe.Pointer(appName))
|
||||
//return C.GoString(appName)
|
||||
// appName := C.getAppName()
|
||||
// defer C.free(unsafe.Pointer(appName))
|
||||
// return C.GoString(appName)
|
||||
return ""
|
||||
}
|
||||
|
||||
|
|
@ -347,7 +348,9 @@ func (m *windowsApp) reshowSystrays() {
|
|||
m.systrayMapLock.Lock()
|
||||
defer m.systrayMapLock.Unlock()
|
||||
for _, systray := range m.systrayMap {
|
||||
systray.reshow()
|
||||
if _, err := systray.show(); err != nil {
|
||||
globalApplication.warning("failed to re-add system tray icon: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,20 +4,18 @@ package application
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/icons"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||
)
|
||||
|
||||
const (
|
||||
WM_USER_SYSTRAY = w32.WM_USER + 1
|
||||
wmUserSystray = w32.WM_USER + 1
|
||||
)
|
||||
|
||||
type windowsSystemTray struct {
|
||||
|
|
@ -25,12 +23,30 @@ type windowsSystemTray struct {
|
|||
|
||||
menu *Win32Menu
|
||||
|
||||
// Platform specific implementation
|
||||
uid uint32
|
||||
hwnd w32.HWND
|
||||
lightModeIcon w32.HICON
|
||||
darkModeIcon w32.HICON
|
||||
currentIcon w32.HICON
|
||||
cancelTheme func()
|
||||
uid uint32
|
||||
hwnd w32.HWND
|
||||
|
||||
lightModeIcon w32.HICON
|
||||
lightModeIconOwned bool
|
||||
darkModeIcon w32.HICON
|
||||
darkModeIconOwned bool
|
||||
currentIcon w32.HICON
|
||||
currentIconOwned bool
|
||||
}
|
||||
|
||||
// releaseIcon destroys an icon handle only when we own it and no new handle reuses it.
|
||||
// Shared handles (e.g. from LoadIcon/LoadIconWithResourceID) must not be passed to DestroyIcon per https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-destroyicon.
|
||||
func (s *windowsSystemTray) releaseIcon(handle w32.HICON, owned bool, keep ...w32.HICON) {
|
||||
if !owned || handle == 0 {
|
||||
return
|
||||
}
|
||||
for _, k := range keep {
|
||||
if handle == k {
|
||||
return
|
||||
}
|
||||
}
|
||||
w32.DestroyIcon(handle)
|
||||
}
|
||||
|
||||
func (s *windowsSystemTray) openMenu() {
|
||||
|
|
@ -89,6 +105,9 @@ func (s *windowsSystemTray) positionWindow(window Window, offset int) error {
|
|||
}
|
||||
|
||||
taskbarBounds := w32.GetTaskbarPosition()
|
||||
if taskbarBounds == nil {
|
||||
return errors.New("failed to get taskbar position")
|
||||
}
|
||||
|
||||
// Set the window position based on the icon location
|
||||
// if the icon is in the taskbar (traybounds) then we need
|
||||
|
|
@ -124,7 +143,7 @@ func (s *windowsSystemTray) bounds() (*Rect, error) {
|
|||
if s.hwnd == 0 {
|
||||
return nil, errors.New("system tray window handle not initialized")
|
||||
}
|
||||
|
||||
|
||||
bounds, err := w32.GetSystrayBounds(s.hwnd, s.uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -150,7 +169,7 @@ func (s *windowsSystemTray) iconIsInTrayBounds() (bool, error) {
|
|||
if s.hwnd == 0 {
|
||||
return false, errors.New("system tray window handle not initialized")
|
||||
}
|
||||
|
||||
|
||||
bounds, err := w32.GetSystrayBounds(s.hwnd, s.uid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
|
@ -199,79 +218,91 @@ func (s *windowsSystemTray) run() {
|
|||
0,
|
||||
nil)
|
||||
if s.hwnd == 0 {
|
||||
panic(syscall.GetLastError())
|
||||
globalApplication.fatal("failed to create tray window: %s", syscall.GetLastError())
|
||||
return
|
||||
}
|
||||
|
||||
nid := w32.NOTIFYICONDATA{
|
||||
HWnd: s.hwnd,
|
||||
UID: uint32(s.parent.id),
|
||||
UFlags: w32.NIF_ICON | w32.NIF_MESSAGE,
|
||||
HIcon: s.currentIcon,
|
||||
UCallbackMessage: WM_USER_SYSTRAY,
|
||||
}
|
||||
nid.CbSize = uint32(unsafe.Sizeof(nid))
|
||||
s.uid = uint32(s.parent.id)
|
||||
|
||||
for retries := range 6 {
|
||||
if !w32.ShellNotifyIcon(w32.NIM_ADD, &nid) {
|
||||
if retries == 5 {
|
||||
globalApplication.fatal("failed to register system tray icon: %w", syscall.GetLastError())
|
||||
}
|
||||
// Resolve the base icons once so we can reuse them for light/dark modes
|
||||
defaultIcon := getNativeApplication().windowClass.Icon
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
nid.UVersion = w32.NOTIFYICON_VERSION
|
||||
|
||||
if !w32.ShellNotifyIcon(w32.NIM_SETVERSION, &nid) {
|
||||
panic(syscall.GetLastError())
|
||||
}
|
||||
|
||||
// Get the application icon if available
|
||||
defaultIcon := w32.LoadIconWithResourceID(w32.GetModuleHandle(""), w32.RT_ICON)
|
||||
if defaultIcon != 0 {
|
||||
s.lightModeIcon = defaultIcon
|
||||
s.darkModeIcon = defaultIcon
|
||||
} else {
|
||||
s.lightModeIcon = lo.Must(w32.CreateSmallHIconFromImage(icons.SystrayLight))
|
||||
s.darkModeIcon = lo.Must(w32.CreateSmallHIconFromImage(icons.SystrayDark))
|
||||
}
|
||||
|
||||
// Use custom icons if provided
|
||||
// Priority: custom icon > default app icon > built-in icon
|
||||
if s.parent.icon != nil {
|
||||
// Create a new icon and destroy the old one
|
||||
newIcon := lo.Must(w32.CreateSmallHIconFromImage(s.parent.icon))
|
||||
if s.lightModeIcon != 0 && s.lightModeIcon != defaultIcon {
|
||||
w32.DestroyIcon(s.lightModeIcon)
|
||||
icon, err := w32.CreateSmallHIconFromImage(s.parent.icon)
|
||||
if err == nil {
|
||||
s.lightModeIcon = icon
|
||||
s.lightModeIconOwned = true
|
||||
} else {
|
||||
globalApplication.warning("failed to create systray icon: %v", err)
|
||||
}
|
||||
s.lightModeIcon = newIcon
|
||||
}
|
||||
|
||||
if s.lightModeIcon == 0 && defaultIcon != 0 {
|
||||
s.lightModeIcon = defaultIcon
|
||||
s.lightModeIconOwned = false
|
||||
}
|
||||
|
||||
if s.lightModeIcon == 0 {
|
||||
icon, err := w32.CreateSmallHIconFromImage(icons.SystrayLight)
|
||||
if err != nil {
|
||||
globalApplication.warning("failed to create systray icon: %v", err)
|
||||
s.lightModeIcon = 0
|
||||
s.lightModeIconOwned = false
|
||||
} else {
|
||||
s.lightModeIcon = icon
|
||||
s.lightModeIconOwned = true
|
||||
}
|
||||
}
|
||||
|
||||
if s.parent.darkModeIcon != nil {
|
||||
// Create a new icon and destroy the old one
|
||||
newIcon := lo.Must(w32.CreateSmallHIconFromImage(s.parent.darkModeIcon))
|
||||
if s.darkModeIcon != 0 && s.darkModeIcon != defaultIcon && s.darkModeIcon != s.lightModeIcon {
|
||||
w32.DestroyIcon(s.darkModeIcon)
|
||||
icon, err := w32.CreateSmallHIconFromImage(s.parent.darkModeIcon)
|
||||
if err == nil {
|
||||
s.darkModeIcon = icon
|
||||
s.darkModeIconOwned = true
|
||||
} else {
|
||||
globalApplication.warning("failed to create systray dark mode icon: %v", err)
|
||||
}
|
||||
s.darkModeIcon = newIcon
|
||||
}
|
||||
s.uid = nid.UID
|
||||
|
||||
if s.darkModeIcon == 0 && s.parent.icon != nil && s.lightModeIcon != 0 {
|
||||
s.darkModeIcon = s.lightModeIcon
|
||||
s.darkModeIconOwned = false
|
||||
}
|
||||
|
||||
if s.darkModeIcon == 0 && defaultIcon != 0 {
|
||||
s.darkModeIcon = defaultIcon
|
||||
s.darkModeIconOwned = false
|
||||
}
|
||||
|
||||
if s.darkModeIcon == 0 {
|
||||
icon, err := w32.CreateSmallHIconFromImage(icons.SystrayDark)
|
||||
if err != nil {
|
||||
globalApplication.warning("failed to create systray dark mode icon: %v", err)
|
||||
s.darkModeIcon = 0
|
||||
s.darkModeIconOwned = false
|
||||
} else {
|
||||
s.darkModeIcon = icon
|
||||
s.darkModeIconOwned = true
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := s.show(); err != nil {
|
||||
// Initial systray add can fail when the shell is not available. This is handled downstream via TaskbarCreated message.
|
||||
globalApplication.warning("initial systray add failed: %v", err)
|
||||
}
|
||||
|
||||
if s.parent.menu != nil {
|
||||
s.updateMenu(s.parent.menu)
|
||||
}
|
||||
|
||||
if s.parent.tooltip != "" {
|
||||
s.setTooltip(s.parent.tooltip)
|
||||
}
|
||||
|
||||
// Set Default Callbacks
|
||||
if s.parent.clickHandler == nil {
|
||||
s.parent.clickHandler = func() {
|
||||
globalApplication.debug("Left Button Clicked")
|
||||
}
|
||||
}
|
||||
|
||||
if s.parent.rightClickHandler == nil {
|
||||
s.parent.rightClickHandler = func() {
|
||||
if s.menu != nil {
|
||||
|
|
@ -280,11 +311,8 @@ func (s *windowsSystemTray) run() {
|
|||
}
|
||||
}
|
||||
|
||||
// Update the icon
|
||||
s.updateIcon()
|
||||
|
||||
// Listen for dark mode changes
|
||||
globalApplication.Event.OnApplicationEvent(events.Windows.SystemThemeChanged, func(event *ApplicationEvent) {
|
||||
s.cancelTheme = globalApplication.Event.OnApplicationEvent(events.Windows.SystemThemeChanged, func(event *ApplicationEvent) {
|
||||
s.updateIcon()
|
||||
})
|
||||
|
||||
|
|
@ -305,6 +333,7 @@ func (s *windowsSystemTray) updateIcon() {
|
|||
|
||||
// Store the old icon to destroy it after updating
|
||||
oldIcon := s.currentIcon
|
||||
oldIconOwned := s.currentIconOwned
|
||||
|
||||
s.currentIcon = newIcon
|
||||
nid := s.newNotifyIconData()
|
||||
|
|
@ -317,10 +346,19 @@ func (s *windowsSystemTray) updateIcon() {
|
|||
panic(syscall.GetLastError())
|
||||
}
|
||||
|
||||
// Destroy the old icon handle if it exists and is not one of our default icons
|
||||
if oldIcon != 0 && oldIcon != s.lightModeIcon && oldIcon != s.darkModeIcon {
|
||||
w32.DestroyIcon(oldIcon)
|
||||
// Track ownership of the current icon so we know if we can destroy it later
|
||||
currentOwned := false
|
||||
if newIcon != 0 {
|
||||
if newIcon == s.lightModeIcon && s.lightModeIconOwned {
|
||||
currentOwned = true
|
||||
} else if newIcon == s.darkModeIcon && s.darkModeIconOwned {
|
||||
currentOwned = true
|
||||
}
|
||||
}
|
||||
s.currentIconOwned = currentOwned
|
||||
|
||||
// Destroy the old icon handle if it exists, we owned it, and nothing else references it
|
||||
s.releaseIcon(oldIcon, oldIconOwned, s.lightModeIcon, s.darkModeIcon)
|
||||
}
|
||||
|
||||
func (s *windowsSystemTray) newNotifyIconData() w32.NOTIFYICONDATA {
|
||||
|
|
@ -333,36 +371,65 @@ func (s *windowsSystemTray) newNotifyIconData() w32.NOTIFYICONDATA {
|
|||
}
|
||||
|
||||
func (s *windowsSystemTray) setIcon(icon []byte) {
|
||||
var err error
|
||||
// Destroy the previous light mode icon if it exists
|
||||
if s.lightModeIcon != 0 {
|
||||
w32.DestroyIcon(s.lightModeIcon)
|
||||
}
|
||||
s.lightModeIcon, err = w32.CreateSmallHIconFromImage(icon)
|
||||
newIcon, err := w32.CreateSmallHIconFromImage(icon)
|
||||
if err != nil {
|
||||
panic(syscall.GetLastError())
|
||||
globalApplication.error("failed to create systray light mode icon: %v", err)
|
||||
return
|
||||
}
|
||||
if s.darkModeIcon == 0 {
|
||||
s.darkModeIcon = s.lightModeIcon
|
||||
|
||||
oldLight := s.lightModeIcon
|
||||
oldLightOwned := s.lightModeIconOwned
|
||||
oldDark := s.darkModeIcon
|
||||
oldDarkOwned := s.darkModeIconOwned
|
||||
|
||||
s.lightModeIcon = newIcon
|
||||
s.lightModeIconOwned = true
|
||||
|
||||
// Keep dark mode in sync when both modes shared the same handle (or dark was unset).
|
||||
if s.darkModeIcon == 0 || s.darkModeIcon == oldLight {
|
||||
s.darkModeIcon = newIcon
|
||||
s.darkModeIconOwned = false
|
||||
}
|
||||
// Update the icon
|
||||
|
||||
// Only free previous handles we own that are no longer referenced.
|
||||
s.releaseIcon(oldLight, oldLightOwned, s.lightModeIcon, s.darkModeIcon)
|
||||
if oldDark != s.darkModeIcon {
|
||||
s.releaseIcon(oldDark, oldDarkOwned, s.lightModeIcon, s.darkModeIcon)
|
||||
}
|
||||
|
||||
s.updateIcon()
|
||||
}
|
||||
|
||||
func (s *windowsSystemTray) setDarkModeIcon(icon []byte) {
|
||||
var err error
|
||||
// Destroy the previous dark mode icon if it exists
|
||||
if s.darkModeIcon != 0 {
|
||||
w32.DestroyIcon(s.darkModeIcon)
|
||||
}
|
||||
s.darkModeIcon, err = w32.CreateSmallHIconFromImage(icon)
|
||||
newIcon, err := w32.CreateSmallHIconFromImage(icon)
|
||||
if err != nil {
|
||||
panic(syscall.GetLastError())
|
||||
globalApplication.error("failed to create systray dark mode icon: %v", err)
|
||||
return
|
||||
}
|
||||
if s.lightModeIcon == 0 {
|
||||
s.lightModeIcon = s.darkModeIcon
|
||||
|
||||
oldDark := s.darkModeIcon
|
||||
oldDarkOwned := s.darkModeIconOwned
|
||||
oldLight := s.lightModeIcon
|
||||
oldLightOwned := s.lightModeIconOwned
|
||||
|
||||
s.darkModeIcon = newIcon
|
||||
s.darkModeIconOwned = true
|
||||
|
||||
lightReplaced := false
|
||||
|
||||
// Keep light mode in sync when both modes shared the same handle (or light was unset).
|
||||
if s.lightModeIcon == 0 || s.lightModeIcon == oldDark {
|
||||
s.lightModeIcon = newIcon
|
||||
s.lightModeIconOwned = false
|
||||
lightReplaced = true
|
||||
}
|
||||
// Update the icon
|
||||
|
||||
// Only free the previous handle if nothing else keeps a reference to it.
|
||||
s.releaseIcon(oldDark, oldDarkOwned, s.lightModeIcon, s.darkModeIcon)
|
||||
if lightReplaced {
|
||||
s.releaseIcon(oldLight, oldLightOwned, s.lightModeIcon, s.darkModeIcon)
|
||||
}
|
||||
|
||||
s.updateIcon()
|
||||
}
|
||||
|
||||
|
|
@ -374,7 +441,7 @@ func newSystemTrayImpl(parent *SystemTray) systemTrayImpl {
|
|||
|
||||
func (s *windowsSystemTray) wndProc(msg uint32, wParam, lParam uintptr) uintptr {
|
||||
switch msg {
|
||||
case WM_USER_SYSTRAY:
|
||||
case wmUserSystray:
|
||||
msg := lParam & 0xffff
|
||||
switch msg {
|
||||
case w32.WM_LBUTTONUP:
|
||||
|
|
@ -393,11 +460,11 @@ func (s *windowsSystemTray) wndProc(msg uint32, wParam, lParam uintptr) uintptr
|
|||
if s.parent.rightDoubleClickHandler != nil {
|
||||
s.parent.rightDoubleClickHandler()
|
||||
}
|
||||
case 0x0406:
|
||||
case w32.NIN_POPUPOPEN:
|
||||
if s.parent.mouseEnterHandler != nil {
|
||||
s.parent.mouseEnterHandler()
|
||||
}
|
||||
case 0x0407:
|
||||
case w32.NIN_POPUPCLOSE:
|
||||
if s.parent.mouseLeaveHandler != nil {
|
||||
s.parent.mouseLeaveHandler()
|
||||
}
|
||||
|
|
@ -407,8 +474,7 @@ func (s *windowsSystemTray) wndProc(msg uint32, wParam, lParam uintptr) uintptr
|
|||
// Menu processing
|
||||
case w32.WM_COMMAND:
|
||||
cmdMsgID := int(wParam & 0xffff)
|
||||
switch cmdMsgID {
|
||||
default:
|
||||
if s.menu != nil {
|
||||
s.menu.ProcessCommand(cmdMsgID)
|
||||
}
|
||||
default:
|
||||
|
|
@ -426,17 +492,14 @@ func (s *windowsSystemTray) updateMenu(menu *Menu) {
|
|||
s.menu.Update()
|
||||
}
|
||||
|
||||
// Based on the idea from https://github.com/wailsapp/wails/issues/3487#issuecomment-2633242304
|
||||
func (s *windowsSystemTray) setTooltip(tooltip string) {
|
||||
// Ensure the tooltip length is within the limit (64 characters for szTip)
|
||||
if len(tooltip) > 64 {
|
||||
tooltip = tooltip[:64]
|
||||
}
|
||||
|
||||
// Create a new NOTIFYICONDATA structure
|
||||
nid := s.newNotifyIconData()
|
||||
nid.UFlags = w32.NIF_TIP
|
||||
tooltipUTF16, err := w32.StringToUTF16(tooltip)
|
||||
nid.UFlags = w32.NIF_TIP | w32.NIF_SHOWTIP
|
||||
|
||||
// Ensure the tooltip length is within the limit (128 characters including null terminate characters for szTip for Windows 2000 and later)
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataw
|
||||
tooltipUTF16, err := w32.StringToUTF16(truncateUTF16(tooltip, 127))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -447,10 +510,6 @@ func (s *windowsSystemTray) setTooltip(tooltip string) {
|
|||
if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) {
|
||||
return
|
||||
}
|
||||
nid.UVersion = 3 // Version 4 does not suport
|
||||
if !w32.ShellNotifyIcon(w32.NIM_SETVERSION, &nid) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Unsupported ----
|
||||
|
|
@ -465,12 +524,16 @@ func (s *windowsSystemTray) setIconPosition(position IconPosition) {
|
|||
}
|
||||
|
||||
func (s *windowsSystemTray) destroy() {
|
||||
if s.cancelTheme != nil {
|
||||
s.cancelTheme()
|
||||
s.cancelTheme = nil
|
||||
}
|
||||
// Remove and delete the system tray
|
||||
getNativeApplication().unregisterSystemTray(s)
|
||||
if s.menu != nil {
|
||||
s.menu.Destroy()
|
||||
}
|
||||
w32.DestroyWindow(s.hwnd)
|
||||
|
||||
// destroy the notification icon
|
||||
nid := s.newNotifyIconData()
|
||||
if !w32.ShellNotifyIcon(w32.NIM_DELETE, &nid) {
|
||||
|
|
@ -478,37 +541,96 @@ func (s *windowsSystemTray) destroy() {
|
|||
}
|
||||
|
||||
// Clean up icon handles
|
||||
if s.lightModeIcon != 0 {
|
||||
w32.DestroyIcon(s.lightModeIcon)
|
||||
s.lightModeIcon = 0
|
||||
}
|
||||
if s.darkModeIcon != 0 && s.darkModeIcon != s.lightModeIcon {
|
||||
w32.DestroyIcon(s.darkModeIcon)
|
||||
s.darkModeIcon = 0
|
||||
}
|
||||
lightIcon := s.lightModeIcon
|
||||
darkIcon := s.darkModeIcon
|
||||
currentIcon := s.currentIcon
|
||||
|
||||
s.releaseIcon(lightIcon, s.lightModeIconOwned)
|
||||
s.releaseIcon(darkIcon, s.darkModeIconOwned, lightIcon)
|
||||
s.releaseIcon(currentIcon, s.currentIconOwned, lightIcon, darkIcon)
|
||||
|
||||
s.lightModeIcon = 0
|
||||
s.lightModeIconOwned = false
|
||||
s.darkModeIcon = 0
|
||||
s.darkModeIconOwned = false
|
||||
s.currentIcon = 0
|
||||
s.currentIconOwned = false
|
||||
|
||||
w32.DestroyWindow(s.hwnd)
|
||||
s.hwnd = 0
|
||||
}
|
||||
|
||||
func (s *windowsSystemTray) Show() {
|
||||
// No-op
|
||||
if s.hwnd == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
nid := s.newNotifyIconData()
|
||||
nid.UFlags = w32.NIF_STATE
|
||||
nid.DwStateMask = w32.NIS_HIDDEN
|
||||
nid.DwState = 0
|
||||
if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) {
|
||||
globalApplication.debug("ShellNotifyIcon NIM_MODIFY show failed: %v", syscall.GetLastError())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *windowsSystemTray) Hide() {
|
||||
// No-op
|
||||
if s.hwnd == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
nid := s.newNotifyIconData()
|
||||
nid.UFlags = w32.NIF_STATE
|
||||
nid.DwStateMask = w32.NIS_HIDDEN
|
||||
nid.DwState = w32.NIS_HIDDEN
|
||||
if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) {
|
||||
globalApplication.debug("ShellNotifyIcon NIM_MODIFY hide failed: %v", syscall.GetLastError())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *windowsSystemTray) reshow() {
|
||||
// Add icons back to systray
|
||||
nid := w32.NOTIFYICONDATA{
|
||||
HWnd: s.hwnd,
|
||||
UID: uint32(s.parent.id),
|
||||
UFlags: w32.NIF_ICON | w32.NIF_MESSAGE,
|
||||
HIcon: s.currentIcon,
|
||||
UCallbackMessage: WM_USER_SYSTRAY,
|
||||
}
|
||||
nid.CbSize = uint32(unsafe.Sizeof(nid))
|
||||
// Show the icon
|
||||
func (s *windowsSystemTray) show() (w32.NOTIFYICONDATA, error) {
|
||||
nid := s.newNotifyIconData()
|
||||
nid.UFlags = w32.NIF_ICON | w32.NIF_MESSAGE
|
||||
nid.HIcon = s.currentIcon
|
||||
nid.UCallbackMessage = wmUserSystray
|
||||
|
||||
if !w32.ShellNotifyIcon(w32.NIM_ADD, &nid) {
|
||||
panic(syscall.GetLastError())
|
||||
err := syscall.GetLastError()
|
||||
return nid, fmt.Errorf("ShellNotifyIcon NIM_ADD failed: %w", err)
|
||||
}
|
||||
|
||||
nid.UVersion = w32.NOTIFYICON_VERSION_4
|
||||
if !w32.ShellNotifyIcon(w32.NIM_SETVERSION, &nid) {
|
||||
err := syscall.GetLastError()
|
||||
return nid, fmt.Errorf("ShellNotifyIcon NIM_SETVERSION failed: %w", err)
|
||||
}
|
||||
|
||||
s.updateIcon()
|
||||
|
||||
if s.parent.tooltip != "" {
|
||||
s.setTooltip(s.parent.tooltip)
|
||||
}
|
||||
|
||||
return nid, nil
|
||||
}
|
||||
|
||||
func truncateUTF16(s string, maxUnits int) string {
|
||||
var units int
|
||||
for i, r := range s {
|
||||
var u int
|
||||
|
||||
// check if rune will take 2 UTF-16 units
|
||||
if r > 0xFFFF {
|
||||
u = 2
|
||||
} else {
|
||||
u = 1
|
||||
}
|
||||
|
||||
if units+u > maxUnits {
|
||||
return s[:i]
|
||||
}
|
||||
units += u
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2607,20 +2607,31 @@ const (
|
|||
MAX_PATH = 260
|
||||
)
|
||||
|
||||
const (
|
||||
NOTIFYICON_VERSION = 3
|
||||
NOTIFYICON_VERSION_4 = 4
|
||||
)
|
||||
|
||||
const (
|
||||
NIM_ADD = 0x00000000
|
||||
NIM_MODIFY = 0x00000001
|
||||
NIM_DELETE = 0x00000002
|
||||
NIM_SETVERSION = 0x00000004
|
||||
|
||||
NIF_MESSAGE = 0x00000001
|
||||
NIF_ICON = 0x00000002
|
||||
NIF_TIP = 0x00000004
|
||||
NIF_STATE = 0x00000008
|
||||
NIF_INFO = 0x00000010
|
||||
NIF_MESSAGE = 0x00000001
|
||||
NIF_ICON = 0x00000002
|
||||
NIF_TIP = 0x00000004
|
||||
NIF_STATE = 0x00000008
|
||||
NIF_INFO = 0x00000010
|
||||
NIF_GUID = 0x00000020
|
||||
NIF_REALTIME = 0x00000040
|
||||
NIF_SHOWTIP = 0x00000080
|
||||
|
||||
NIS_HIDDEN = 0x00000001
|
||||
|
||||
NIN_POPUPOPEN = WM_USER + 6
|
||||
NIN_POPUPCLOSE = WM_USER + 7
|
||||
|
||||
NIIF_NONE = 0x00000000
|
||||
NIIF_INFO = 0x00000001
|
||||
NIIF_WARNING = 0x00000002
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ package w32
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/sys/windows"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type CSIDL uint32
|
||||
|
|
@ -79,8 +80,6 @@ const (
|
|||
CSIDL_FLAG_NO_ALIAS = 0x1000
|
||||
CSIDL_FLAG_PER_USER_INIT = 0x8000
|
||||
CSIDL_FLAG_MASK = 0xFF00
|
||||
|
||||
NOTIFYICON_VERSION = 4
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue