[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:
DeltaLaboratory 2025-11-04 05:44:58 +09:00 committed by GitHub
commit d58d4ba758
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 336 additions and 136 deletions

View file

@ -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 -->

View 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)
}
}

View file

@ -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)
}
} }
} }

View file

@ -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
@ -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
} }

View file

@ -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

View file

@ -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 (