mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +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
|
## Added
|
||||||
<!-- New features, capabilities, or enhancements -->
|
<!-- New features, capabilities, or enhancements -->
|
||||||
|
- Add `systray-clock` example showing a headless tray with live tooltip updates (#4653).
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
<!-- Changes in existing functionality -->
|
<!-- 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
|
## Fixed
|
||||||
<!-- Bug fixes -->
|
<!-- 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
|
## Deprecated
|
||||||
<!-- Soon-to-be removed features -->
|
<!-- 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"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/wailsapp/go-webview2/webviewloader"
|
"github.com/wailsapp/go-webview2/webviewloader"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v3/internal/operatingsystem"
|
"github.com/wailsapp/wails/v3/internal/operatingsystem"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v3/pkg/events"
|
"github.com/wailsapp/wails/v3/pkg/events"
|
||||||
|
|
@ -124,9 +125,9 @@ func (m *windowsApp) setIcon(_ []byte) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *windowsApp) name() string {
|
func (m *windowsApp) name() string {
|
||||||
//appName := C.getAppName()
|
// appName := C.getAppName()
|
||||||
//defer C.free(unsafe.Pointer(appName))
|
// defer C.free(unsafe.Pointer(appName))
|
||||||
//return C.GoString(appName)
|
// return C.GoString(appName)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -347,7 +348,9 @@ func (m *windowsApp) reshowSystrays() {
|
||||||
m.systrayMapLock.Lock()
|
m.systrayMapLock.Lock()
|
||||||
defer m.systrayMapLock.Unlock()
|
defer m.systrayMapLock.Unlock()
|
||||||
for _, systray := range m.systrayMap {
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v3/pkg/icons"
|
"github.com/wailsapp/wails/v3/pkg/icons"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v3/pkg/events"
|
"github.com/wailsapp/wails/v3/pkg/events"
|
||||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
WM_USER_SYSTRAY = w32.WM_USER + 1
|
wmUserSystray = w32.WM_USER + 1
|
||||||
)
|
)
|
||||||
|
|
||||||
type windowsSystemTray struct {
|
type windowsSystemTray struct {
|
||||||
|
|
@ -25,12 +23,30 @@ type windowsSystemTray struct {
|
||||||
|
|
||||||
menu *Win32Menu
|
menu *Win32Menu
|
||||||
|
|
||||||
// Platform specific implementation
|
cancelTheme func()
|
||||||
uid uint32
|
uid uint32
|
||||||
hwnd w32.HWND
|
hwnd w32.HWND
|
||||||
lightModeIcon w32.HICON
|
|
||||||
darkModeIcon w32.HICON
|
lightModeIcon w32.HICON
|
||||||
currentIcon 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() {
|
func (s *windowsSystemTray) openMenu() {
|
||||||
|
|
@ -89,6 +105,9 @@ func (s *windowsSystemTray) positionWindow(window Window, offset int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
taskbarBounds := w32.GetTaskbarPosition()
|
taskbarBounds := w32.GetTaskbarPosition()
|
||||||
|
if taskbarBounds == nil {
|
||||||
|
return errors.New("failed to get taskbar position")
|
||||||
|
}
|
||||||
|
|
||||||
// Set the window position based on the icon location
|
// Set the window position based on the icon location
|
||||||
// if the icon is in the taskbar (traybounds) then we need
|
// 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 {
|
if s.hwnd == 0 {
|
||||||
return nil, errors.New("system tray window handle not initialized")
|
return nil, errors.New("system tray window handle not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
bounds, err := w32.GetSystrayBounds(s.hwnd, s.uid)
|
bounds, err := w32.GetSystrayBounds(s.hwnd, s.uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -150,7 +169,7 @@ func (s *windowsSystemTray) iconIsInTrayBounds() (bool, error) {
|
||||||
if s.hwnd == 0 {
|
if s.hwnd == 0 {
|
||||||
return false, errors.New("system tray window handle not initialized")
|
return false, errors.New("system tray window handle not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
bounds, err := w32.GetSystrayBounds(s.hwnd, s.uid)
|
bounds, err := w32.GetSystrayBounds(s.hwnd, s.uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|
@ -199,79 +218,91 @@ func (s *windowsSystemTray) run() {
|
||||||
0,
|
0,
|
||||||
nil)
|
nil)
|
||||||
if s.hwnd == 0 {
|
if s.hwnd == 0 {
|
||||||
panic(syscall.GetLastError())
|
globalApplication.fatal("failed to create tray window: %s", syscall.GetLastError())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nid := w32.NOTIFYICONDATA{
|
s.uid = uint32(s.parent.id)
|
||||||
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))
|
|
||||||
|
|
||||||
for retries := range 6 {
|
// Resolve the base icons once so we can reuse them for light/dark modes
|
||||||
if !w32.ShellNotifyIcon(w32.NIM_ADD, &nid) {
|
defaultIcon := getNativeApplication().windowClass.Icon
|
||||||
if retries == 5 {
|
|
||||||
globalApplication.fatal("failed to register system tray icon: %w", syscall.GetLastError())
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
// Priority: custom icon > default app icon > built-in icon
|
||||||
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
|
|
||||||
if s.parent.icon != nil {
|
if s.parent.icon != nil {
|
||||||
// Create a new icon and destroy the old one
|
icon, err := w32.CreateSmallHIconFromImage(s.parent.icon)
|
||||||
newIcon := lo.Must(w32.CreateSmallHIconFromImage(s.parent.icon))
|
if err == nil {
|
||||||
if s.lightModeIcon != 0 && s.lightModeIcon != defaultIcon {
|
s.lightModeIcon = icon
|
||||||
w32.DestroyIcon(s.lightModeIcon)
|
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 {
|
if s.parent.darkModeIcon != nil {
|
||||||
// Create a new icon and destroy the old one
|
icon, err := w32.CreateSmallHIconFromImage(s.parent.darkModeIcon)
|
||||||
newIcon := lo.Must(w32.CreateSmallHIconFromImage(s.parent.darkModeIcon))
|
if err == nil {
|
||||||
if s.darkModeIcon != 0 && s.darkModeIcon != defaultIcon && s.darkModeIcon != s.lightModeIcon {
|
s.darkModeIcon = icon
|
||||||
w32.DestroyIcon(s.darkModeIcon)
|
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 {
|
if s.parent.menu != nil {
|
||||||
s.updateMenu(s.parent.menu)
|
s.updateMenu(s.parent.menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.parent.tooltip != "" {
|
|
||||||
s.setTooltip(s.parent.tooltip)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set Default Callbacks
|
// Set Default Callbacks
|
||||||
if s.parent.clickHandler == nil {
|
if s.parent.clickHandler == nil {
|
||||||
s.parent.clickHandler = func() {
|
s.parent.clickHandler = func() {
|
||||||
globalApplication.debug("Left Button Clicked")
|
globalApplication.debug("Left Button Clicked")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.parent.rightClickHandler == nil {
|
if s.parent.rightClickHandler == nil {
|
||||||
s.parent.rightClickHandler = func() {
|
s.parent.rightClickHandler = func() {
|
||||||
if s.menu != nil {
|
if s.menu != nil {
|
||||||
|
|
@ -280,11 +311,8 @@ func (s *windowsSystemTray) run() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the icon
|
|
||||||
s.updateIcon()
|
|
||||||
|
|
||||||
// Listen for dark mode changes
|
// 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()
|
s.updateIcon()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -305,6 +333,7 @@ func (s *windowsSystemTray) updateIcon() {
|
||||||
|
|
||||||
// Store the old icon to destroy it after updating
|
// Store the old icon to destroy it after updating
|
||||||
oldIcon := s.currentIcon
|
oldIcon := s.currentIcon
|
||||||
|
oldIconOwned := s.currentIconOwned
|
||||||
|
|
||||||
s.currentIcon = newIcon
|
s.currentIcon = newIcon
|
||||||
nid := s.newNotifyIconData()
|
nid := s.newNotifyIconData()
|
||||||
|
|
@ -317,10 +346,19 @@ func (s *windowsSystemTray) updateIcon() {
|
||||||
panic(syscall.GetLastError())
|
panic(syscall.GetLastError())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destroy the old icon handle if it exists and is not one of our default icons
|
// Track ownership of the current icon so we know if we can destroy it later
|
||||||
if oldIcon != 0 && oldIcon != s.lightModeIcon && oldIcon != s.darkModeIcon {
|
currentOwned := false
|
||||||
w32.DestroyIcon(oldIcon)
|
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 {
|
func (s *windowsSystemTray) newNotifyIconData() w32.NOTIFYICONDATA {
|
||||||
|
|
@ -333,36 +371,65 @@ func (s *windowsSystemTray) newNotifyIconData() w32.NOTIFYICONDATA {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *windowsSystemTray) setIcon(icon []byte) {
|
func (s *windowsSystemTray) setIcon(icon []byte) {
|
||||||
var err error
|
newIcon, err := w32.CreateSmallHIconFromImage(icon)
|
||||||
// Destroy the previous light mode icon if it exists
|
|
||||||
if s.lightModeIcon != 0 {
|
|
||||||
w32.DestroyIcon(s.lightModeIcon)
|
|
||||||
}
|
|
||||||
s.lightModeIcon, err = w32.CreateSmallHIconFromImage(icon)
|
|
||||||
if err != nil {
|
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()
|
s.updateIcon()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *windowsSystemTray) setDarkModeIcon(icon []byte) {
|
func (s *windowsSystemTray) setDarkModeIcon(icon []byte) {
|
||||||
var err error
|
newIcon, err := w32.CreateSmallHIconFromImage(icon)
|
||||||
// Destroy the previous dark mode icon if it exists
|
|
||||||
if s.darkModeIcon != 0 {
|
|
||||||
w32.DestroyIcon(s.darkModeIcon)
|
|
||||||
}
|
|
||||||
s.darkModeIcon, err = w32.CreateSmallHIconFromImage(icon)
|
|
||||||
if err != nil {
|
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()
|
s.updateIcon()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -374,7 +441,7 @@ func newSystemTrayImpl(parent *SystemTray) systemTrayImpl {
|
||||||
|
|
||||||
func (s *windowsSystemTray) wndProc(msg uint32, wParam, lParam uintptr) uintptr {
|
func (s *windowsSystemTray) wndProc(msg uint32, wParam, lParam uintptr) uintptr {
|
||||||
switch msg {
|
switch msg {
|
||||||
case WM_USER_SYSTRAY:
|
case wmUserSystray:
|
||||||
msg := lParam & 0xffff
|
msg := lParam & 0xffff
|
||||||
switch msg {
|
switch msg {
|
||||||
case w32.WM_LBUTTONUP:
|
case w32.WM_LBUTTONUP:
|
||||||
|
|
@ -393,11 +460,11 @@ func (s *windowsSystemTray) wndProc(msg uint32, wParam, lParam uintptr) uintptr
|
||||||
if s.parent.rightDoubleClickHandler != nil {
|
if s.parent.rightDoubleClickHandler != nil {
|
||||||
s.parent.rightDoubleClickHandler()
|
s.parent.rightDoubleClickHandler()
|
||||||
}
|
}
|
||||||
case 0x0406:
|
case w32.NIN_POPUPOPEN:
|
||||||
if s.parent.mouseEnterHandler != nil {
|
if s.parent.mouseEnterHandler != nil {
|
||||||
s.parent.mouseEnterHandler()
|
s.parent.mouseEnterHandler()
|
||||||
}
|
}
|
||||||
case 0x0407:
|
case w32.NIN_POPUPCLOSE:
|
||||||
if s.parent.mouseLeaveHandler != nil {
|
if s.parent.mouseLeaveHandler != nil {
|
||||||
s.parent.mouseLeaveHandler()
|
s.parent.mouseLeaveHandler()
|
||||||
}
|
}
|
||||||
|
|
@ -407,8 +474,7 @@ func (s *windowsSystemTray) wndProc(msg uint32, wParam, lParam uintptr) uintptr
|
||||||
// Menu processing
|
// Menu processing
|
||||||
case w32.WM_COMMAND:
|
case w32.WM_COMMAND:
|
||||||
cmdMsgID := int(wParam & 0xffff)
|
cmdMsgID := int(wParam & 0xffff)
|
||||||
switch cmdMsgID {
|
if s.menu != nil {
|
||||||
default:
|
|
||||||
s.menu.ProcessCommand(cmdMsgID)
|
s.menu.ProcessCommand(cmdMsgID)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
@ -426,17 +492,14 @@ func (s *windowsSystemTray) updateMenu(menu *Menu) {
|
||||||
s.menu.Update()
|
s.menu.Update()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Based on the idea from https://github.com/wailsapp/wails/issues/3487#issuecomment-2633242304
|
|
||||||
func (s *windowsSystemTray) setTooltip(tooltip string) {
|
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
|
// Create a new NOTIFYICONDATA structure
|
||||||
nid := s.newNotifyIconData()
|
nid := s.newNotifyIconData()
|
||||||
nid.UFlags = w32.NIF_TIP
|
nid.UFlags = w32.NIF_TIP | w32.NIF_SHOWTIP
|
||||||
tooltipUTF16, err := w32.StringToUTF16(tooltip)
|
|
||||||
|
// 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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -447,10 +510,6 @@ func (s *windowsSystemTray) setTooltip(tooltip string) {
|
||||||
if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) {
|
if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
nid.UVersion = 3 // Version 4 does not suport
|
|
||||||
if !w32.ShellNotifyIcon(w32.NIM_SETVERSION, &nid) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Unsupported ----
|
// ---- Unsupported ----
|
||||||
|
|
@ -465,12 +524,16 @@ func (s *windowsSystemTray) setIconPosition(position IconPosition) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *windowsSystemTray) destroy() {
|
func (s *windowsSystemTray) destroy() {
|
||||||
|
if s.cancelTheme != nil {
|
||||||
|
s.cancelTheme()
|
||||||
|
s.cancelTheme = nil
|
||||||
|
}
|
||||||
// Remove and delete the system tray
|
// Remove and delete the system tray
|
||||||
getNativeApplication().unregisterSystemTray(s)
|
getNativeApplication().unregisterSystemTray(s)
|
||||||
if s.menu != nil {
|
if s.menu != nil {
|
||||||
s.menu.Destroy()
|
s.menu.Destroy()
|
||||||
}
|
}
|
||||||
w32.DestroyWindow(s.hwnd)
|
|
||||||
// destroy the notification icon
|
// destroy the notification icon
|
||||||
nid := s.newNotifyIconData()
|
nid := s.newNotifyIconData()
|
||||||
if !w32.ShellNotifyIcon(w32.NIM_DELETE, &nid) {
|
if !w32.ShellNotifyIcon(w32.NIM_DELETE, &nid) {
|
||||||
|
|
@ -478,37 +541,96 @@ func (s *windowsSystemTray) destroy() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up icon handles
|
// Clean up icon handles
|
||||||
if s.lightModeIcon != 0 {
|
lightIcon := s.lightModeIcon
|
||||||
w32.DestroyIcon(s.lightModeIcon)
|
darkIcon := s.darkModeIcon
|
||||||
s.lightModeIcon = 0
|
currentIcon := s.currentIcon
|
||||||
}
|
|
||||||
if s.darkModeIcon != 0 && s.darkModeIcon != s.lightModeIcon {
|
s.releaseIcon(lightIcon, s.lightModeIconOwned)
|
||||||
w32.DestroyIcon(s.darkModeIcon)
|
s.releaseIcon(darkIcon, s.darkModeIconOwned, lightIcon)
|
||||||
s.darkModeIcon = 0
|
s.releaseIcon(currentIcon, s.currentIconOwned, lightIcon, darkIcon)
|
||||||
}
|
|
||||||
|
s.lightModeIcon = 0
|
||||||
|
s.lightModeIconOwned = false
|
||||||
|
s.darkModeIcon = 0
|
||||||
|
s.darkModeIconOwned = false
|
||||||
s.currentIcon = 0
|
s.currentIcon = 0
|
||||||
|
s.currentIconOwned = false
|
||||||
|
|
||||||
|
w32.DestroyWindow(s.hwnd)
|
||||||
|
s.hwnd = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *windowsSystemTray) Show() {
|
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() {
|
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() {
|
func (s *windowsSystemTray) show() (w32.NOTIFYICONDATA, error) {
|
||||||
// Add icons back to systray
|
nid := s.newNotifyIconData()
|
||||||
nid := w32.NOTIFYICONDATA{
|
nid.UFlags = w32.NIF_ICON | w32.NIF_MESSAGE
|
||||||
HWnd: s.hwnd,
|
nid.HIcon = s.currentIcon
|
||||||
UID: uint32(s.parent.id),
|
nid.UCallbackMessage = wmUserSystray
|
||||||
UFlags: w32.NIF_ICON | w32.NIF_MESSAGE,
|
|
||||||
HIcon: s.currentIcon,
|
|
||||||
UCallbackMessage: WM_USER_SYSTRAY,
|
|
||||||
}
|
|
||||||
nid.CbSize = uint32(unsafe.Sizeof(nid))
|
|
||||||
// Show the icon
|
|
||||||
if !w32.ShellNotifyIcon(w32.NIM_ADD, &nid) {
|
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
|
MAX_PATH = 260
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
NOTIFYICON_VERSION = 3
|
||||||
|
NOTIFYICON_VERSION_4 = 4
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NIM_ADD = 0x00000000
|
NIM_ADD = 0x00000000
|
||||||
NIM_MODIFY = 0x00000001
|
NIM_MODIFY = 0x00000001
|
||||||
NIM_DELETE = 0x00000002
|
NIM_DELETE = 0x00000002
|
||||||
NIM_SETVERSION = 0x00000004
|
NIM_SETVERSION = 0x00000004
|
||||||
|
|
||||||
NIF_MESSAGE = 0x00000001
|
NIF_MESSAGE = 0x00000001
|
||||||
NIF_ICON = 0x00000002
|
NIF_ICON = 0x00000002
|
||||||
NIF_TIP = 0x00000004
|
NIF_TIP = 0x00000004
|
||||||
NIF_STATE = 0x00000008
|
NIF_STATE = 0x00000008
|
||||||
NIF_INFO = 0x00000010
|
NIF_INFO = 0x00000010
|
||||||
|
NIF_GUID = 0x00000020
|
||||||
|
NIF_REALTIME = 0x00000040
|
||||||
|
NIF_SHOWTIP = 0x00000080
|
||||||
|
|
||||||
NIS_HIDDEN = 0x00000001
|
NIS_HIDDEN = 0x00000001
|
||||||
|
|
||||||
|
NIN_POPUPOPEN = WM_USER + 6
|
||||||
|
NIN_POPUPCLOSE = WM_USER + 7
|
||||||
|
|
||||||
NIIF_NONE = 0x00000000
|
NIIF_NONE = 0x00000000
|
||||||
NIIF_INFO = 0x00000001
|
NIIF_INFO = 0x00000001
|
||||||
NIIF_WARNING = 0x00000002
|
NIIF_WARNING = 0x00000002
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,10 @@ package w32
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CSIDL uint32
|
type CSIDL uint32
|
||||||
|
|
@ -79,8 +80,6 @@ const (
|
||||||
CSIDL_FLAG_NO_ALIAS = 0x1000
|
CSIDL_FLAG_NO_ALIAS = 0x1000
|
||||||
CSIDL_FLAG_PER_USER_INIT = 0x8000
|
CSIDL_FLAG_PER_USER_INIT = 0x8000
|
||||||
CSIDL_FLAG_MASK = 0xFF00
|
CSIDL_FLAG_MASK = 0xFF00
|
||||||
|
|
||||||
NOTIFYICON_VERSION = 4
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue