返回 DeepSeek-Reasonix
systray_windows.go
根目录 / desktop / third_party / systray / systray_windows.go
1 //go:build windows
2
3 package systray
4
5 import (
6 "crypto/md5"
7 "encoding/hex"
8 "errors"
9 "fmt"
10 "io/ioutil"
11 "log"
12 "os"
13 "path/filepath"
14 "sort"
15 "strings"
16 "sync"
17 "sync/atomic"
18 "syscall"
19 "unsafe"
20
21 "golang.org/x/sys/windows"
22 )
23
24 // Helpful sources: https://github.com/golang/exp/blob/master/shiny/driver/internal/win32
25
26 var (
27 g32 = windows.NewLazySystemDLL("Gdi32.dll")
28 pCreateCompatibleBitmap = g32.NewProc("CreateCompatibleBitmap")
29 pCreateCompatibleDC = g32.NewProc("CreateCompatibleDC")
30 pCreateDIBSection = g32.NewProc("CreateDIBSection")
31 pDeleteDC = g32.NewProc("DeleteDC")
32 pSelectObject = g32.NewProc("SelectObject")
33
34 k32 = windows.NewLazySystemDLL("Kernel32.dll")
35 pGetModuleHandle = k32.NewProc("GetModuleHandleW")
36
37 s32 = windows.NewLazySystemDLL("Shell32.dll")
38 pShellNotifyIcon = s32.NewProc("Shell_NotifyIconW")
39
40 u32 = windows.NewLazySystemDLL("User32.dll")
41 pCreateMenu = u32.NewProc("CreateMenu")
42 pCreatePopupMenu = u32.NewProc("CreatePopupMenu")
43 pCreateWindowEx = u32.NewProc("CreateWindowExW")
44 pDefWindowProc = u32.NewProc("DefWindowProcW")
45 pDeleteMenu = u32.NewProc("DeleteMenu")
46 pDestroyMenu = u32.NewProc("DestroyMenu")
47 pRemoveMenu = u32.NewProc("RemoveMenu")
48 pDestroyWindow = u32.NewProc("DestroyWindow")
49 pDispatchMessage = u32.NewProc("DispatchMessageW")
50 pDrawIconEx = u32.NewProc("DrawIconEx")
51 pGetCursorPos = u32.NewProc("GetCursorPos")
52 pGetDC = u32.NewProc("GetDC")
53 pGetMessage = u32.NewProc("GetMessageW")
54 pGetSystemMetrics = u32.NewProc("GetSystemMetrics")
55 pInsertMenuItem = u32.NewProc("InsertMenuItemW")
56 pLoadCursor = u32.NewProc("LoadCursorW")
57 pLoadIcon = u32.NewProc("LoadIconW")
58 pLoadImage = u32.NewProc("LoadImageW")
59 pPostMessage = u32.NewProc("PostMessageW")
60 pPostQuitMessage = u32.NewProc("PostQuitMessage")
61 pRegisterClass = u32.NewProc("RegisterClassExW")
62 pRegisterWindowMessage = u32.NewProc("RegisterWindowMessageW")
63 pReleaseDC = u32.NewProc("ReleaseDC")
64 pSetForegroundWindow = u32.NewProc("SetForegroundWindow")
65 pSetMenuInfo = u32.NewProc("SetMenuInfo")
66 pSetMenuItemInfo = u32.NewProc("SetMenuItemInfoW")
67 pShowWindow = u32.NewProc("ShowWindow")
68 pTrackPopupMenu = u32.NewProc("TrackPopupMenu")
69 pTranslateMessage = u32.NewProc("TranslateMessage")
70 pUnregisterClass = u32.NewProc("UnregisterClassW")
71 pUpdateWindow = u32.NewProc("UpdateWindow")
72
73 // ErrTrayNotReadyYet is returned by functions when they are called before the tray has been initialized.
74 ErrTrayNotReadyYet = errors.New("tray not ready yet")
75 )
76
77 // Contains window class information.
78 // It is used with the RegisterClassEx and GetClassInfoEx functions.
79 // https://msdn.microsoft.com/en-us/library/ms633577.aspx
80 type wndClassEx struct {
81 Size, Style uint32
82 WndProc uintptr
83 ClsExtra, WndExtra int32
84 Instance, Icon, Cursor, Background windows.Handle
85 MenuName, ClassName *uint16
86 IconSm windows.Handle
87 }
88
89 // Registers a window class for subsequent use in calls to the CreateWindow or CreateWindowEx function.
90 // https://msdn.microsoft.com/en-us/library/ms633587.aspx
91 func (w *wndClassEx) register() error {
92 w.Size = uint32(unsafe.Sizeof(*w))
93 res, _, err := pRegisterClass.Call(uintptr(unsafe.Pointer(w)))
94 if res == 0 {
95 return err
96 }
97 return nil
98 }
99
100 // Unregisters a window class, freeing the memory required for the class.
101 // https://msdn.microsoft.com/en-us/library/ms644899.aspx
102 func (w *wndClassEx) unregister() error {
103 res, _, err := pUnregisterClass.Call(
104 uintptr(unsafe.Pointer(w.ClassName)),
105 uintptr(w.Instance),
106 )
107 if res == 0 {
108 return err
109 }
110 return nil
111 }
112
113 // Contains information that the system needs to display notifications in the notification area.
114 // Used by Shell_NotifyIcon.
115 // https://msdn.microsoft.com/en-us/library/windows/desktop/bb773352(v=vs.85).aspx
116 // https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159
117 type notifyIconData struct {
118 Size uint32
119 Wnd windows.Handle
120 ID, Flags, CallbackMessage uint32
121 Icon windows.Handle
122 Tip [128]uint16
123 State, StateMask uint32
124 Info [256]uint16
125 TimeoutOrVersion uint32 // uTimeout and uVersion share one native union.
126 InfoTitle [64]uint16
127 InfoFlags uint32
128 GuidItem windows.GUID
129 BalloonIcon windows.Handle
130 }
131
132 var shellNotifyIcon = pShellNotifyIcon.Call
133
134 func (nid *notifyIconData) add() error {
135 const NIM_ADD = 0x00000000
136 res, _, err := shellNotifyIcon(
137 uintptr(NIM_ADD),
138 uintptr(unsafe.Pointer(nid)),
139 )
140 if res == 0 {
141 return err
142 }
143 return nil
144 }
145
146 func (nid *notifyIconData) modify() error {
147 const NIM_MODIFY = 0x00000001
148 res, _, err := shellNotifyIcon(
149 uintptr(NIM_MODIFY),
150 uintptr(unsafe.Pointer(nid)),
151 )
152 if res == 0 {
153 return err
154 }
155 return nil
156 }
157
158 func (nid *notifyIconData) delete() error {
159 const NIM_DELETE = 0x00000002
160 res, _, err := shellNotifyIcon(
161 uintptr(NIM_DELETE),
162 uintptr(unsafe.Pointer(nid)),
163 )
164 if res == 0 {
165 return err
166 }
167 return nil
168 }
169
170 // Contains information about a menu item.
171 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx
172 type menuItemInfo struct {
173 Size, Mask, Type, State uint32
174 ID uint32
175 SubMenu, Checked, Unchecked windows.Handle
176 ItemData uintptr
177 TypeData *uint16
178 Cch uint32
179 BMPItem windows.Handle
180 }
181
182 // The POINT structure defines the x- and y- coordinates of a point.
183 // https://msdn.microsoft.com/en-us/library/windows/desktop/dd162805(v=vs.85).aspx
184 type point struct {
185 X, Y int32
186 }
187
188 // The BITMAPINFO structure defines the dimensions and color information for a DIB.
189 // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfo
190 type bitmapInfo struct {
191 BmiHeader bitmapInfoHeader
192 BmiColors windows.Handle
193 }
194
195 // The BITMAPINFOHEADER structure contains information about the dimensions and color format of a device-independent bitmap (DIB).
196 // https://learn.microsoft.com/en-us/previous-versions/dd183376(v=vs.85)
197 // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
198 type bitmapInfoHeader struct {
199 BiSize uint32
200 BiWidth int32
201 BiHeight int32
202 BiPlanes uint16
203 BiBitCount uint16
204 BiCompression uint32
205 BiSizeImage uint32
206 BiXPelsPerMeter int32
207 BiYPelsPerMeter int32
208 BiClrUsed uint32
209 BiClrImportant uint32
210 }
211
212 // Contains information about loaded resources
213 type winTray struct {
214 instance,
215 icon,
216 cursor,
217 window windows.Handle
218
219 loadedImages map[string]windows.Handle
220 muLoadedImages sync.RWMutex
221 // menus keeps track of the submenus keyed by the menu item ID, plus 0
222 // which corresponds to the main popup menu.
223 menus map[uint32]windows.Handle
224 muMenus sync.RWMutex
225 // menuOf keeps track of the menu each menu item belongs to.
226 menuOf map[uint32]windows.Handle
227 muMenuOf sync.RWMutex
228 // menuItemIcons maintains the bitmap of each menu item (if applies). It's
229 // needed to show the icon correctly when showing a previously hidden menu
230 // item again.
231 menuItemIcons map[uint32]windows.Handle
232 muMenuItemIcons sync.RWMutex
233 visibleItems map[uint32][]uint32
234 muVisibleItems sync.RWMutex
235
236 nid *notifyIconData
237 muNID sync.RWMutex
238 wcex *wndClassEx
239
240 wmSystrayMessage,
241 wmTaskbarCreated uint32
242
243 initialized atomic.Bool
244 iconGUID windows.GUID
245 useIconGUID bool
246 }
247
248 // isReady checks if the tray as already been initialized. It is not goroutine safe with in regard to the initialization function, but prevents a panic when functions are called too early.
249 func (t *winTray) isReady() bool {
250 return t.initialized.Load()
251 }
252
253 // Loads an image from file and shows it in tray.
254 // Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx
255 func (t *winTray) setIcon(src string) error {
256 if !wt.isReady() {
257 return ErrTrayNotReadyYet
258 }
259
260 const NIF_ICON = 0x00000002
261
262 h, err := t.loadIconFrom(src)
263 if err != nil {
264 return err
265 }
266
267 t.muNID.Lock()
268 defer t.muNID.Unlock()
269 t.nid.Icon = h
270 t.nid.Flags |= NIF_ICON
271 t.nid.Size = uint32(unsafe.Sizeof(*t.nid))
272
273 return t.nid.modify()
274 }
275
276 // Sets tooltip on icon.
277 // Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx
278 func (t *winTray) setTooltip(src string) error {
279 if !wt.isReady() {
280 return ErrTrayNotReadyYet
281 }
282
283 const NIF_TIP = 0x00000004
284 b, err := windows.UTF16FromString(src)
285 if err != nil {
286 return err
287 }
288
289 t.muNID.Lock()
290 defer t.muNID.Unlock()
291 copy(t.nid.Tip[:], b[:])
292 t.nid.Flags |= NIF_TIP
293 t.nid.Size = uint32(unsafe.Sizeof(*t.nid))
294
295 return t.nid.modify()
296 }
297
298 var wt = winTray{}
299
300 // WindowProc callback function that processes messages sent to a window.
301 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633573(v=vs.85).aspx
302 func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam uintptr) (lResult uintptr) {
303 const (
304 WM_RBUTTONUP = 0x0205
305 WM_LBUTTONUP = 0x0202
306 WM_COMMAND = 0x0111
307 WM_ENDSESSION = 0x0016
308 WM_CLOSE = 0x0010
309 WM_DESTROY = 0x0002
310 )
311 switch message {
312 case WM_COMMAND:
313 menuItemId := int32(wParam)
314 // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-command#menus
315 if menuItemId != -1 {
316 systrayMenuItemSelected(uint32(wParam))
317 }
318 case WM_CLOSE:
319 pDestroyWindow.Call(uintptr(t.window))
320 t.wcex.unregister()
321 case WM_DESTROY:
322 // same as WM_ENDSESSION, but throws 0 exit code after all
323 defer pPostQuitMessage.Call(uintptr(int32(0)))
324 fallthrough
325 case WM_ENDSESSION:
326 t.muNID.Lock()
327 if t.nid != nil {
328 t.nid.delete()
329 }
330 t.muNID.Unlock()
331 runSystrayExit()
332 case t.wmSystrayMessage:
333 switch lParam {
334 case WM_LBUTTONUP:
335 systrayLeftClick()
336 case WM_RBUTTONUP:
337 systrayRightClick()
338 }
339 case t.wmTaskbarCreated: // on explorer.exe restarts
340 t.muNID.Lock()
341 t.nid.add()
342 t.muNID.Unlock()
343 default:
344 // Calls the default window procedure to provide default processing for any window messages that an application does not process.
345 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633572(v=vs.85).aspx
346 lResult, _, _ = pDefWindowProc.Call(
347 uintptr(hWnd),
348 uintptr(message),
349 uintptr(wParam),
350 uintptr(lParam),
351 )
352 }
353 return
354 }
355
356 func (t *winTray) initInstance() error {
357 const IDI_APPLICATION = 32512
358 const IDC_ARROW = 32512 // Standard arrow
359 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633548(v=vs.85).aspx
360 const SW_HIDE = 0
361 const CW_USEDEFAULT = 0x80000000
362 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms632600(v=vs.85).aspx
363 const (
364 WS_CAPTION = 0x00C00000
365 WS_MAXIMIZEBOX = 0x00010000
366 WS_MINIMIZEBOX = 0x00020000
367 WS_OVERLAPPED = 0x00000000
368 WS_SYSMENU = 0x00080000
369 WS_THICKFRAME = 0x00040000
370
371 WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
372 )
373 // https://msdn.microsoft.com/en-us/library/windows/desktop/ff729176
374 const (
375 CS_HREDRAW = 0x0002
376 CS_VREDRAW = 0x0001
377 )
378 const (
379 NIF_MESSAGE = 0x00000001
380 NIF_GUID = 0x00000020
381 )
382
383 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644931(v=vs.85).aspx
384 const WM_USER = 0x0400
385
386 const (
387 className = "SystrayClass"
388 windowName = ""
389 )
390
391 t.wmSystrayMessage = WM_USER + 1
392 t.visibleItems = make(map[uint32][]uint32)
393 t.menus = make(map[uint32]windows.Handle)
394 t.menuOf = make(map[uint32]windows.Handle)
395 t.menuItemIcons = make(map[uint32]windows.Handle)
396
397 taskbarEventNamePtr, _ := windows.UTF16PtrFromString("TaskbarCreated")
398 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644947
399 res, _, err := pRegisterWindowMessage.Call(
400 uintptr(unsafe.Pointer(taskbarEventNamePtr)),
401 )
402 t.wmTaskbarCreated = uint32(res)
403
404 t.loadedImages = make(map[string]windows.Handle)
405
406 instanceHandle, _, err := pGetModuleHandle.Call(0)
407 if instanceHandle == 0 {
408 return err
409 }
410 t.instance = windows.Handle(instanceHandle)
411
412 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms648072(v=vs.85).aspx
413 iconHandle, _, err := pLoadIcon.Call(0, uintptr(IDI_APPLICATION))
414 if iconHandle == 0 {
415 return err
416 }
417 t.icon = windows.Handle(iconHandle)
418
419 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms648391(v=vs.85).aspx
420 cursorHandle, _, err := pLoadCursor.Call(0, uintptr(IDC_ARROW))
421 if cursorHandle == 0 {
422 return err
423 }
424 t.cursor = windows.Handle(cursorHandle)
425
426 classNamePtr, err := windows.UTF16PtrFromString(className)
427 if err != nil {
428 return err
429 }
430
431 windowNamePtr, err := windows.UTF16PtrFromString(windowName)
432 if err != nil {
433 return err
434 }
435
436 t.wcex = &wndClassEx{
437 Style: CS_HREDRAW | CS_VREDRAW,
438 WndProc: windows.NewCallback(t.wndProc),
439 Instance: t.instance,
440 Icon: t.icon,
441 Cursor: t.cursor,
442 Background: windows.Handle(6), // (COLOR_WINDOW + 1)
443 ClassName: classNamePtr,
444 IconSm: t.icon,
445 }
446 if err := t.wcex.register(); err != nil {
447 return err
448 }
449
450 windowHandle, _, err := pCreateWindowEx.Call(
451 uintptr(0),
452 uintptr(unsafe.Pointer(classNamePtr)),
453 uintptr(unsafe.Pointer(windowNamePtr)),
454 uintptr(WS_OVERLAPPEDWINDOW),
455 uintptr(CW_USEDEFAULT),
456 uintptr(CW_USEDEFAULT),
457 uintptr(CW_USEDEFAULT),
458 uintptr(CW_USEDEFAULT),
459 uintptr(0),
460 uintptr(0),
461 uintptr(t.instance),
462 uintptr(0),
463 )
464 if windowHandle == 0 {
465 return err
466 }
467 t.window = windows.Handle(windowHandle)
468
469 pShowWindow.Call(
470 uintptr(t.window),
471 uintptr(SW_HIDE),
472 )
473
474 pUpdateWindow.Call(
475 uintptr(t.window),
476 )
477
478 t.muNID.Lock()
479 defer t.muNID.Unlock()
480 flags := uint32(NIF_MESSAGE)
481 if t.useIconGUID {
482 flags |= NIF_GUID
483 }
484 t.nid = &notifyIconData{
485 Wnd: windows.Handle(t.window),
486 ID: 100,
487 Flags: flags,
488 CallbackMessage: t.wmSystrayMessage,
489 GuidItem: t.iconGUID,
490 }
491 t.nid.Size = uint32(unsafe.Sizeof(*t.nid))
492
493 return t.addInitialIcon()
494 }
495
496 // addInitialIcon may fall back only before this process owns any GUID icon.
497 // Never delete the rejected GUID: another process may own it.
498 func (t *winTray) addInitialIcon() error {
499 err := t.nid.add()
500 if err == nil || !t.useIconGUID {
501 return err
502 }
503 t.useIconGUID = false
504 t.iconGUID = windows.GUID{}
505 t.nid.GuidItem = windows.GUID{}
506 t.nid.Flags &^= 0x20 // NIF_GUID
507 return t.nid.add()
508 }
509
510 // SetIconID configures a stable Windows notification-area identity. It must be
511 // called before Run so every add, modify, delete, and Explorer restart uses the
512 // same GUID.
513 func SetIconID(id string) error {
514 guid, err := windows.GUIDFromString(id)
515 if err != nil {
516 return fmt.Errorf("invalid tray icon GUID: %w", err)
517 }
518 wt.muNID.Lock()
519 defer wt.muNID.Unlock()
520 if wt.initialized.Load() || wt.nid != nil {
521 return fmt.Errorf("tray icon GUID must be set before Run")
522 }
523 wt.iconGUID = guid
524 wt.useIconGUID = true
525 return nil
526 }
527
528 func (t *winTray) createMenu() error {
529 const MIM_APPLYTOSUBMENUS = 0x80000000 // Settings apply to the menu and all of its submenus
530
531 menuHandle, _, err := pCreatePopupMenu.Call()
532 if menuHandle == 0 {
533 return err
534 }
535 t.menus[0] = windows.Handle(menuHandle)
536
537 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647575(v=vs.85).aspx
538 mi := struct {
539 Size, Mask, Style, Max uint32
540 Background windows.Handle
541 ContextHelpID uint32
542 MenuData uintptr
543 }{
544 Mask: MIM_APPLYTOSUBMENUS,
545 }
546 mi.Size = uint32(unsafe.Sizeof(mi))
547
548 res, _, err := pSetMenuInfo.Call(
549 uintptr(t.menus[0]),
550 uintptr(unsafe.Pointer(&mi)),
551 )
552 if res == 0 {
553 return err
554 }
555 return nil
556 }
557
558 func (t *winTray) convertToSubMenu(menuItemId uint32) (windows.Handle, error) {
559 const MIIM_SUBMENU = 0x00000004
560
561 res, _, err := pCreateMenu.Call()
562 if res == 0 {
563 return 0, err
564 }
565 menu := windows.Handle(res)
566
567 mi := menuItemInfo{Mask: MIIM_SUBMENU, SubMenu: menu}
568 mi.Size = uint32(unsafe.Sizeof(mi))
569 t.muMenuOf.RLock()
570 hMenu := t.menuOf[menuItemId]
571 t.muMenuOf.RUnlock()
572 res, _, err = pSetMenuItemInfo.Call(
573 uintptr(hMenu),
574 uintptr(menuItemId),
575 0,
576 uintptr(unsafe.Pointer(&mi)),
577 )
578 if res == 0 {
579 return 0, err
580 }
581 t.muMenus.Lock()
582 t.menus[menuItemId] = menu
583 t.muMenus.Unlock()
584 return menu, nil
585 }
586
587 // SetRemovalAllowed sets whether a user can remove the systray icon or not.
588 // This is only supported on macOS.
589 func SetRemovalAllowed(allowed bool) {
590 }
591
592 // winKeyNames maps the platform neutral key names used by SetShortcut to the names
593 // that are commonly presented in Windows menus.
594 var winKeyNames = map[string]string{
595 "BackSpace": "Backspace",
596 "Delete": "Del",
597 "Enter": "Enter",
598 "Escape": "Esc",
599 "Insert": "Ins",
600 "PageDown": "PgDn",
601 "PageUp": "PgUp",
602 "Return": "Enter",
603 }
604
605 // shortcutText returns the accelerator text presented after the item label,
606 // for example "Ctrl+Shift+S". It is empty if the item has no shortcut.
607 func (item *MenuItem) shortcutText() string {
608 if item.shortcutKey == "" {
609 return ""
610 }
611
612 b := strings.Builder{}
613 if item.shortcutMods&KeyModifierControl != 0 {
614 b.WriteString("Ctrl+")
615 }
616 if item.shortcutMods&KeyModifierAlt != 0 {
617 b.WriteString("Alt+")
618 }
619 if item.shortcutMods&KeyModifierShift != 0 {
620 b.WriteString("Shift+")
621 }
622 if item.shortcutMods&KeyModifierSuper != 0 {
623 b.WriteString("Win+")
624 }
625
626 if key, ok := winKeyNames[item.shortcutKey]; ok {
627 b.WriteString(key)
628 } else {
629 b.WriteString(strings.ToUpper(item.shortcutKey))
630 }
631 return b.String()
632 }
633
634 func (t *winTray) addOrUpdateMenuItem(menuItemId uint32, parentId uint32, title, shortcut string, disabled, checked bool) error {
635 if !wt.isReady() {
636 return ErrTrayNotReadyYet
637 }
638
639 if shortcut != "" {
640 // Windows right aligns any text that follows a tab character in a menu item
641 title += "\t" + shortcut
642 }
643
644 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx
645 const (
646 MIIM_FTYPE = 0x00000100
647 MIIM_BITMAP = 0x00000080
648 MIIM_STRING = 0x00000040
649 MIIM_SUBMENU = 0x00000004
650 MIIM_ID = 0x00000002
651 MIIM_STATE = 0x00000001
652 )
653 const MFT_STRING = 0x00000000
654 const (
655 MFS_CHECKED = 0x00000008
656 MFS_DISABLED = 0x00000003
657 )
658 titlePtr, err := windows.UTF16PtrFromString(title)
659 if err != nil {
660 return err
661 }
662
663 mi := menuItemInfo{
664 Mask: MIIM_FTYPE | MIIM_STRING | MIIM_ID | MIIM_STATE,
665 Type: MFT_STRING,
666 ID: uint32(menuItemId),
667 TypeData: titlePtr,
668 Cch: uint32(len(title)),
669 }
670 mi.Size = uint32(unsafe.Sizeof(mi))
671 if disabled {
672 mi.State |= MFS_DISABLED
673 }
674 if checked {
675 mi.State |= MFS_CHECKED
676 }
677 t.muMenuItemIcons.RLock()
678 hIcon := t.menuItemIcons[menuItemId]
679 t.muMenuItemIcons.RUnlock()
680 if hIcon > 0 {
681 mi.Mask |= MIIM_BITMAP
682 mi.BMPItem = hIcon
683 }
684
685 var res uintptr
686 t.muMenus.RLock()
687 menu, exists := t.menus[parentId]
688 t.muMenus.RUnlock()
689 if !exists {
690 menu, err = t.convertToSubMenu(parentId)
691 if err != nil {
692 return err
693 }
694 t.muMenus.Lock()
695 t.menus[parentId] = menu
696 t.muMenus.Unlock()
697 } else if t.getVisibleItemIndex(parentId, menuItemId) != -1 {
698 // We set the menu item info based on the menuID
699 res, _, err = pSetMenuItemInfo.Call(
700 uintptr(menu),
701 uintptr(menuItemId),
702 0,
703 uintptr(unsafe.Pointer(&mi)),
704 )
705 }
706
707 if res == 0 {
708 // Menu item does not already exist, create it
709 t.muMenus.RLock()
710 submenu, exists := t.menus[menuItemId]
711 t.muMenus.RUnlock()
712 if exists {
713 mi.Mask |= MIIM_SUBMENU
714 mi.SubMenu = submenu
715 }
716 t.addToVisibleItems(parentId, menuItemId)
717 position := t.getVisibleItemIndex(parentId, menuItemId)
718 res, _, err = pInsertMenuItem.Call(
719 uintptr(menu),
720 uintptr(position),
721 1,
722 uintptr(unsafe.Pointer(&mi)),
723 )
724 if res == 0 {
725 t.delFromVisibleItems(parentId, menuItemId)
726 return err
727 }
728 t.muMenuOf.Lock()
729 t.menuOf[menuItemId] = menu
730 t.muMenuOf.Unlock()
731 }
732
733 return nil
734 }
735
736 func (t *winTray) addSeparatorMenuItem(menuItemId, parentId uint32) error {
737 if !wt.isReady() {
738 return ErrTrayNotReadyYet
739 }
740
741 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx
742 const (
743 MIIM_FTYPE = 0x00000100
744 MIIM_ID = 0x00000002
745 MIIM_STATE = 0x00000001
746 )
747 const MFT_SEPARATOR = 0x00000800
748
749 mi := menuItemInfo{
750 Mask: MIIM_FTYPE | MIIM_ID | MIIM_STATE,
751 Type: MFT_SEPARATOR,
752 ID: uint32(menuItemId),
753 }
754
755 mi.Size = uint32(unsafe.Sizeof(mi))
756
757 t.addToVisibleItems(parentId, menuItemId)
758 position := t.getVisibleItemIndex(parentId, menuItemId)
759 t.muMenus.RLock()
760 menu := uintptr(t.menus[parentId])
761 t.muMenus.RUnlock()
762 res, _, err := pInsertMenuItem.Call(
763 menu,
764 uintptr(position),
765 1,
766 uintptr(unsafe.Pointer(&mi)),
767 )
768 if res == 0 {
769 return err
770 }
771
772 return nil
773 }
774
775 func (t *winTray) removeMenuItem(menuItemId, parentId uint32) error {
776 if !wt.isReady() {
777 return ErrTrayNotReadyYet
778 }
779
780 const MF_BYCOMMAND = 0x00000000
781 const ERROR_SUCCESS syscall.Errno = 0
782
783 t.muMenus.RLock()
784 menu := uintptr(t.menus[parentId])
785 t.muMenus.RUnlock()
786 res, _, err := pDeleteMenu.Call(
787 menu,
788 uintptr(menuItemId),
789 MF_BYCOMMAND,
790 )
791 if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS {
792 return err
793 }
794 t.delFromVisibleItems(parentId, menuItemId)
795
796 return nil
797 }
798
799 func (t *winTray) hideMenuItem(menuItemId, parentId uint32) error {
800 if !wt.isReady() {
801 return ErrTrayNotReadyYet
802 }
803
804 const MF_BYCOMMAND = 0x00000000
805 const ERROR_SUCCESS syscall.Errno = 0
806
807 t.muMenus.RLock()
808 menu := uintptr(t.menus[parentId])
809 t.muMenus.RUnlock()
810 res, _, err := pRemoveMenu.Call(
811 menu,
812 uintptr(menuItemId),
813 MF_BYCOMMAND,
814 )
815 if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS {
816 return err
817 }
818 t.delFromVisibleItems(parentId, menuItemId)
819
820 return nil
821 }
822
823 func (t *winTray) showMenu() error {
824 if !wt.isReady() {
825 return ErrTrayNotReadyYet
826 }
827
828 const (
829 TPM_BOTTOMALIGN = 0x0020
830 TPM_LEFTALIGN = 0x0000
831 )
832 p := point{}
833 res, _, err := pGetCursorPos.Call(uintptr(unsafe.Pointer(&p)))
834 if res == 0 {
835 return err
836 }
837 pSetForegroundWindow.Call(uintptr(t.window))
838
839 res, _, err = pTrackPopupMenu.Call(
840 uintptr(t.menus[0]),
841 TPM_BOTTOMALIGN|TPM_LEFTALIGN,
842 uintptr(p.X),
843 uintptr(p.Y),
844 0,
845 uintptr(t.window),
846 0,
847 )
848 if res == 0 {
849 return err
850 }
851
852 return nil
853 }
854
855 func (t *winTray) delFromVisibleItems(parent, val uint32) {
856 t.muVisibleItems.Lock()
857 defer t.muVisibleItems.Unlock()
858 visibleItems := t.visibleItems[parent]
859 for i, itemval := range visibleItems {
860 if val == itemval {
861 t.visibleItems[parent] = append(visibleItems[:i], visibleItems[i+1:]...)
862 break
863 }
864 }
865 }
866
867 func (t *winTray) addToVisibleItems(parent, val uint32) {
868 t.muVisibleItems.Lock()
869 defer t.muVisibleItems.Unlock()
870 if visibleItems, exists := t.visibleItems[parent]; !exists {
871 t.visibleItems[parent] = []uint32{val}
872 } else {
873 newvisible := append(visibleItems, val)
874 sort.Slice(newvisible, func(i, j int) bool { return newvisible[i] < newvisible[j] })
875 t.visibleItems[parent] = newvisible
876 }
877 }
878
879 func (t *winTray) getVisibleItemIndex(parent, val uint32) int {
880 t.muVisibleItems.RLock()
881 defer t.muVisibleItems.RUnlock()
882 for i, itemval := range t.visibleItems[parent] {
883 if val == itemval {
884 return i
885 }
886 }
887 return -1
888 }
889
890 // Loads an image from file to be shown in tray or menu item.
891 // LoadImage: https://msdn.microsoft.com/en-us/library/windows/desktop/ms648045(v=vs.85).aspx
892 func (t *winTray) loadIconFrom(src string) (windows.Handle, error) {
893 if !wt.isReady() {
894 return 0, ErrTrayNotReadyYet
895 }
896
897 const IMAGE_ICON = 1 // Loads an icon
898 const LR_LOADFROMFILE = 0x00000010 // Loads the stand-alone image from the file
899 const LR_DEFAULTSIZE = 0x00000040 // Loads default-size icon for windows(SM_CXICON x SM_CYICON) if cx, cy are set to zero
900
901 // Save and reuse handles of loaded images
902 t.muLoadedImages.RLock()
903 h, ok := t.loadedImages[src]
904 t.muLoadedImages.RUnlock()
905 if !ok {
906 srcPtr, err := windows.UTF16PtrFromString(src)
907 if err != nil {
908 return 0, err
909 }
910 res, _, err := pLoadImage.Call(
911 0,
912 uintptr(unsafe.Pointer(srcPtr)),
913 IMAGE_ICON,
914 0,
915 0,
916 LR_LOADFROMFILE|LR_DEFAULTSIZE,
917 )
918 if res == 0 {
919 return 0, err
920 }
921 h = windows.Handle(res)
922 t.muLoadedImages.Lock()
923 t.loadedImages[src] = h
924 t.muLoadedImages.Unlock()
925 }
926 return h, nil
927 }
928
929 func iconToBitmap(hIcon windows.Handle) (windows.Handle, error) {
930 const SM_CXSMICON = 49
931 const SM_CYSMICON = 50
932 const DI_NORMAL = 0x3
933 hDC, _, err := pGetDC.Call(uintptr(0))
934 if hDC == 0 {
935 return 0, err
936 }
937 defer pReleaseDC.Call(uintptr(0), hDC)
938 hMemDC, _, err := pCreateCompatibleDC.Call(hDC)
939 if hMemDC == 0 {
940 return 0, err
941 }
942 defer pDeleteDC.Call(hMemDC)
943 cx, _, _ := pGetSystemMetrics.Call(SM_CXSMICON)
944 cy, _, _ := pGetSystemMetrics.Call(SM_CYSMICON)
945 hMemBmp, err := create32BitHBitmap(hMemDC, int32(cx), int32(cy))
946 hOriginalBmp, _, _ := pSelectObject.Call(hMemDC, hMemBmp)
947 defer pSelectObject.Call(hMemDC, hOriginalBmp)
948 res, _, err := pDrawIconEx.Call(hMemDC, 0, 0, uintptr(hIcon), cx, cy, 0, uintptr(0), DI_NORMAL)
949 if res == 0 {
950 return 0, err
951 }
952 return windows.Handle(hMemBmp), nil
953 }
954
955 // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createdibsection
956 func create32BitHBitmap(hDC uintptr, cx, cy int32) (uintptr, error) {
957 const BI_RGB uint32 = 0
958 const DIB_RGB_COLORS = 0
959 bmi := bitmapInfo{
960 BmiHeader: bitmapInfoHeader{
961 BiPlanes: 1,
962 BiCompression: BI_RGB,
963 BiWidth: cx,
964 BiHeight: cy,
965 BiBitCount: 32,
966 },
967 }
968 bmi.BmiHeader.BiSize = uint32(unsafe.Sizeof(bmi.BmiHeader))
969 var bits uintptr
970 hBitmap, _, err := pCreateDIBSection.Call(
971 hDC,
972 uintptr(unsafe.Pointer(&bmi)),
973 DIB_RGB_COLORS,
974 uintptr(unsafe.Pointer(&bits)),
975 uintptr(0),
976 0,
977 )
978 if hBitmap == 0 {
979 return 0, err
980 }
981 return hBitmap, nil
982 }
983
984 func registerSystray() {
985 if err := wt.initInstance(); err != nil {
986 log.Printf("systray error: unable to init instance: %s\n", err)
987 return
988 }
989
990 if err := wt.createMenu(); err != nil {
991 log.Printf("systray error: unable to create menu: %s\n", err)
992 return
993 }
994
995 wt.initialized.Store(true)
996 systrayReady()
997 }
998
999 var m = &struct {
1000 WindowHandle windows.Handle
1001 Message uint32
1002 Wparam uintptr
1003 Lparam uintptr
1004 Time uint32
1005 Pt point
1006 }{}
1007
1008 func nativeLoop() {
1009 for doNativeTick() {
1010 }
1011 }
1012
1013 func nativeEnd() {
1014 }
1015
1016 func nativeStart() {
1017 go func() {
1018 for doNativeTick() {
1019 }
1020 }()
1021 }
1022
1023 func doNativeTick() bool {
1024 ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(m)), 0, 0, 0)
1025
1026 // If the function retrieves a message other than WM_QUIT, the return value is nonzero.
1027 // If the function retrieves the WM_QUIT message, the return value is zero.
1028 // If there is an error, the return value is -1
1029 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx
1030 switch int32(ret) {
1031 case -1:
1032 log.Printf("systray error: message loop failure: %s\n", err)
1033 return false
1034 case 0:
1035 return false
1036 default:
1037 pTranslateMessage.Call(uintptr(unsafe.Pointer(m)))
1038 pDispatchMessage.Call(uintptr(unsafe.Pointer(m)))
1039 }
1040 return true
1041 }
1042
1043 func quit() {
1044 const WM_CLOSE = 0x0010
1045
1046 pPostMessage.Call(
1047 uintptr(wt.window),
1048 WM_CLOSE,
1049 0,
1050 0,
1051 )
1052
1053 wt.muNID.Lock()
1054 if wt.nid != nil {
1055 wt.nid.delete()
1056 }
1057 wt.muNID.Unlock()
1058 runSystrayExit()
1059 }
1060
1061 func setInternalLoop(bool) {
1062 }
1063
1064 func iconBytesToFilePath(iconBytes []byte) (string, error) {
1065 bh := md5.Sum(iconBytes)
1066 dataHash := hex.EncodeToString(bh[:])
1067 iconFilePath := filepath.Join(os.TempDir(), "systray_temp_icon_"+dataHash)
1068
1069 if _, err := os.Stat(iconFilePath); os.IsNotExist(err) {
1070 if err := ioutil.WriteFile(iconFilePath, iconBytes, 0644); err != nil {
1071 return "", err
1072 }
1073 }
1074 return iconFilePath, nil
1075 }
1076
1077 // SetIcon sets the systray icon.
1078 // iconBytes should be the content of .ico for windows and .ico/.jpg/.png
1079 // for other platforms.
1080 func SetIcon(iconBytes []byte) {
1081 iconFilePath, err := iconBytesToFilePath(iconBytes)
1082 if err != nil {
1083 log.Printf("systray error: unable to write icon data to temp file: %s\n", err)
1084 return
1085 }
1086 if err := wt.setIcon(iconFilePath); err != nil {
1087 log.Printf("systray error: unable to set icon: %s\n", err)
1088 return
1089 }
1090 }
1091
1092 // SetIconFromFilePath sets the systray icon from a file path.
1093 // iconFilePath should be the path to a .ico for windows and .ico/.jpg/.png for other platforms.
1094 func SetIconFromFilePath(iconFilePath string) error {
1095 err := wt.setIcon(iconFilePath)
1096 if err != nil {
1097 return fmt.Errorf("failed to set icon: %v", err)
1098 }
1099 return nil
1100 }
1101
1102 // SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back
1103 // to a regular icon on other platforms.
1104 // templateIconBytes and iconBytes should be the content of .ico for windows and
1105 // .ico/.jpg/.png for other platforms.
1106 func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
1107 SetIcon(regularIconBytes)
1108 }
1109
1110 // SetTitle sets the systray title, only available on Mac and Linux.
1111 func SetTitle(title string) {
1112 // do nothing
1113 }
1114
1115 func (item *MenuItem) parentId() uint32 {
1116 if item.parent != nil {
1117 return uint32(item.parent.id)
1118 }
1119 return 0
1120 }
1121
1122 // SetIcon sets the icon of a menu item. Only works on macOS and Windows.
1123 // iconBytes should be the content of .ico/.jpg/.png
1124 func (item *MenuItem) SetIcon(iconBytes []byte) {
1125 iconFilePath, err := iconBytesToFilePath(iconBytes)
1126 if err != nil {
1127 log.Printf("systray error: unable to write icon data to temp file: %s\n", err)
1128 return
1129 }
1130
1131 err = item.SetIconFromFilePath(iconFilePath)
1132 if err != nil {
1133 log.Printf("systray error: %s\n", err)
1134 return
1135 }
1136 }
1137
1138 // SetIconFromFilePath sets the icon of a menu item from a file path.
1139 // iconFilePath should be the path to a .ico for windows and .ico/.jpg/.png for other platforms.
1140 func (item *MenuItem) SetIconFromFilePath(iconFilePath string) error {
1141 h, err := wt.loadIconFrom(iconFilePath)
1142 if err != nil {
1143 return fmt.Errorf("unable to load icon from file: %s", err)
1144 }
1145
1146 h, err = iconToBitmap(h)
1147 if err != nil {
1148 return fmt.Errorf("unable to convert icon to bitmap: %s", err)
1149 }
1150 wt.muMenuItemIcons.Lock()
1151 wt.menuItemIcons[uint32(item.id)] = h
1152 wt.muMenuItemIcons.Unlock()
1153
1154 err = wt.addOrUpdateMenuItem(uint32(item.id), item.parentId(), item.title, item.shortcutText(), item.disabled, item.checked)
1155 if err != nil {
1156 return fmt.Errorf("unable to addOrUpdateMenuItem: %s", err)
1157 }
1158 return nil
1159 }
1160
1161 // SetTooltip sets the systray tooltip to display on mouse hover of the tray icon,
1162 // only available on Mac and Windows.
1163 func SetTooltip(tooltip string) {
1164 if err := wt.setTooltip(tooltip); err != nil {
1165 log.Printf("systray error: unable to set tooltip: %s\n", err)
1166 return
1167 }
1168 }
1169
1170 func addOrUpdateMenuItem(item *MenuItem) {
1171 err := wt.addOrUpdateMenuItem(uint32(item.id), item.parentId(), item.title, item.shortcutText(), item.disabled, item.checked)
1172 if err != nil {
1173 log.Printf("systray error: unable to addOrUpdateMenuItem: %s\n", err)
1174 return
1175 }
1176 }
1177
1178 // SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it
1179 // falls back to the regular icon bytes and on Linux it does nothing.
1180 // templateIconBytes and regularIconBytes should be the content of .ico for windows and
1181 // .ico/.jpg/.png for other platforms.
1182 func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
1183 item.SetIcon(regularIconBytes)
1184 }
1185
1186 func addSeparator(id uint32, parent uint32) {
1187 err := wt.addSeparatorMenuItem(id, parent)
1188 if err != nil {
1189 log.Printf("systray error: unable to addSeparator: %s\n", err)
1190 return
1191 }
1192 }
1193
1194 func hideMenuItem(item *MenuItem) {
1195 err := wt.hideMenuItem(uint32(item.id), item.parentId())
1196 if err != nil {
1197 log.Printf("systray error: unable to hideMenuItem: %s\n", err)
1198 return
1199 }
1200 }
1201
1202 func removeMenuItem(item *MenuItem) {
1203 err := wt.removeMenuItem(uint32(item.id), item.parentId())
1204 if err != nil {
1205 log.Printf("systray error: unable to removeMenuItem: %s\n", err)
1206 return
1207 }
1208 }
1209
1210 func showMenuItem(item *MenuItem) {
1211 addOrUpdateMenuItem(item)
1212 }
1213
1214 func resetMenu() {
1215 _, _, _ = pDestroyMenu.Call(uintptr(wt.menus[0]))
1216 wt.visibleItems = make(map[uint32][]uint32)
1217 wt.menus = make(map[uint32]windows.Handle)
1218 wt.menuOf = make(map[uint32]windows.Handle)
1219 wt.menuItemIcons = make(map[uint32]windows.Handle)
1220 wt.createMenu()
1221 }
1222
1223 func systrayLeftClick() {
1224 if fn := tappedLeft; fn != nil {
1225 fn()
1226 return
1227 }
1228
1229 wt.showMenu()
1230 }
1231
1232 func systrayRightClick() {
1233 if fn := tappedRight; fn != nil {
1234 fn()
1235 return
1236 }
1237
1238 wt.showMenu()
1239 }
1240
1240 lines GO