1// +build windows 2 3package systray 4 5import ( 6 "crypto/md5" 7 "encoding/hex" 8 "io/ioutil" 9 "log" 10 "os" 11 "path/filepath" 12 "sort" 13 "sync" 14 "syscall" 15 "unsafe" 16 17 "golang.org/x/sys/windows" 18) 19 20// Helpful sources: https://github.com/golang/exp/blob/master/shiny/driver/internal/win32 21 22var ( 23 g32 = windows.NewLazySystemDLL("Gdi32.dll") 24 pCreateCompatibleBitmap = g32.NewProc("CreateCompatibleBitmap") 25 pCreateCompatibleDC = g32.NewProc("CreateCompatibleDC") 26 pDeleteDC = g32.NewProc("DeleteDC") 27 pSelectObject = g32.NewProc("SelectObject") 28 29 k32 = windows.NewLazySystemDLL("Kernel32.dll") 30 pGetModuleHandle = k32.NewProc("GetModuleHandleW") 31 32 s32 = windows.NewLazySystemDLL("Shell32.dll") 33 pShellNotifyIcon = s32.NewProc("Shell_NotifyIconW") 34 35 u32 = windows.NewLazySystemDLL("User32.dll") 36 pCreateMenu = u32.NewProc("CreateMenu") 37 pCreatePopupMenu = u32.NewProc("CreatePopupMenu") 38 pCreateWindowEx = u32.NewProc("CreateWindowExW") 39 pDefWindowProc = u32.NewProc("DefWindowProcW") 40 pDeleteMenu = u32.NewProc("DeleteMenu") 41 pDestroyWindow = u32.NewProc("DestroyWindow") 42 pDispatchMessage = u32.NewProc("DispatchMessageW") 43 pDrawIconEx = u32.NewProc("DrawIconEx") 44 pGetCursorPos = u32.NewProc("GetCursorPos") 45 pGetDC = u32.NewProc("GetDC") 46 pGetMenuItemID = u32.NewProc("GetMenuItemID") 47 pGetMessage = u32.NewProc("GetMessageW") 48 pGetSystemMetrics = u32.NewProc("GetSystemMetrics") 49 pInsertMenuItem = u32.NewProc("InsertMenuItemW") 50 pLoadCursor = u32.NewProc("LoadCursorW") 51 pLoadIcon = u32.NewProc("LoadIconW") 52 pLoadImage = u32.NewProc("LoadImageW") 53 pPostMessage = u32.NewProc("PostMessageW") 54 pPostQuitMessage = u32.NewProc("PostQuitMessage") 55 pRegisterClass = u32.NewProc("RegisterClassExW") 56 pRegisterWindowMessage = u32.NewProc("RegisterWindowMessageW") 57 pReleaseDC = u32.NewProc("ReleaseDC") 58 pSetForegroundWindow = u32.NewProc("SetForegroundWindow") 59 pSetMenuInfo = u32.NewProc("SetMenuInfo") 60 pSetMenuItemInfo = u32.NewProc("SetMenuItemInfoW") 61 pShowWindow = u32.NewProc("ShowWindow") 62 pTrackPopupMenu = u32.NewProc("TrackPopupMenu") 63 pTranslateMessage = u32.NewProc("TranslateMessage") 64 pUnregisterClass = u32.NewProc("UnregisterClassW") 65 pUpdateWindow = u32.NewProc("UpdateWindow") 66) 67 68// Contains window class information. 69// It is used with the RegisterClassEx and GetClassInfoEx functions. 70// https://msdn.microsoft.com/en-us/library/ms633577.aspx 71type wndClassEx struct { 72 Size, Style uint32 73 WndProc uintptr 74 ClsExtra, WndExtra int32 75 Instance, Icon, Cursor, Background windows.Handle 76 MenuName, ClassName *uint16 77 IconSm windows.Handle 78} 79 80// Registers a window class for subsequent use in calls to the CreateWindow or CreateWindowEx function. 81// https://msdn.microsoft.com/en-us/library/ms633587.aspx 82func (w *wndClassEx) register() error { 83 w.Size = uint32(unsafe.Sizeof(*w)) 84 res, _, err := pRegisterClass.Call(uintptr(unsafe.Pointer(w))) 85 if res == 0 { 86 return err 87 } 88 return nil 89} 90 91// Unregisters a window class, freeing the memory required for the class. 92// https://msdn.microsoft.com/en-us/library/ms644899.aspx 93func (w *wndClassEx) unregister() error { 94 res, _, err := pUnregisterClass.Call( 95 uintptr(unsafe.Pointer(w.ClassName)), 96 uintptr(w.Instance), 97 ) 98 if res == 0 { 99 return err 100 } 101 return nil 102} 103 104// Contains information that the system needs to display notifications in the notification area. 105// Used by Shell_NotifyIcon. 106// https://msdn.microsoft.com/en-us/library/windows/desktop/bb773352(v=vs.85).aspx 107// https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159 108type notifyIconData struct { 109 Size uint32 110 Wnd windows.Handle 111 ID, Flags, CallbackMessage uint32 112 Icon windows.Handle 113 Tip [128]uint16 114 State, StateMask uint32 115 Info [256]uint16 116 Timeout, Version uint32 117 InfoTitle [64]uint16 118 InfoFlags uint32 119 GuidItem windows.GUID 120 BalloonIcon windows.Handle 121} 122 123func (nid *notifyIconData) add() error { 124 const NIM_ADD = 0x00000000 125 res, _, err := pShellNotifyIcon.Call( 126 uintptr(NIM_ADD), 127 uintptr(unsafe.Pointer(nid)), 128 ) 129 if res == 0 { 130 return err 131 } 132 return nil 133} 134 135func (nid *notifyIconData) modify() error { 136 const NIM_MODIFY = 0x00000001 137 res, _, err := pShellNotifyIcon.Call( 138 uintptr(NIM_MODIFY), 139 uintptr(unsafe.Pointer(nid)), 140 ) 141 if res == 0 { 142 return err 143 } 144 return nil 145} 146 147func (nid *notifyIconData) delete() error { 148 const NIM_DELETE = 0x00000002 149 res, _, err := pShellNotifyIcon.Call( 150 uintptr(NIM_DELETE), 151 uintptr(unsafe.Pointer(nid)), 152 ) 153 if res == 0 { 154 return err 155 } 156 return nil 157} 158 159// Contains information about a menu item. 160// https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx 161type menuItemInfo struct { 162 Size, Mask, Type, State uint32 163 ID uint32 164 SubMenu, Checked, Unchecked windows.Handle 165 ItemData uintptr 166 TypeData *uint16 167 Cch uint32 168 BMPItem windows.Handle 169} 170 171// The POINT structure defines the x- and y- coordinates of a point. 172// https://msdn.microsoft.com/en-us/library/windows/desktop/dd162805(v=vs.85).aspx 173type point struct { 174 X, Y int32 175} 176 177// Contains information about loaded resources 178type winTray struct { 179 instance, 180 icon, 181 cursor, 182 window windows.Handle 183 184 loadedImages map[string]windows.Handle 185 muLoadedImages sync.RWMutex 186 // menus keeps track of the submenus keyed by the menu item ID, plus 0 187 // which corresponds to the main popup menu. 188 menus map[uint32]windows.Handle 189 muMenus sync.RWMutex 190 // menuOf keeps track of the menu each menu item belongs to. 191 menuOf map[uint32]windows.Handle 192 muMenuOf sync.RWMutex 193 // menuItemIcons maintains the bitmap of each menu item (if applies). It's 194 // needed to show the icon correctly when showing a previously hidden menu 195 // item again. 196 menuItemIcons map[uint32]windows.Handle 197 muMenuItemIcons sync.RWMutex 198 visibleItems map[uint32][]uint32 199 muVisibleItems sync.RWMutex 200 201 nid *notifyIconData 202 muNID sync.RWMutex 203 wcex *wndClassEx 204 205 wmSystrayMessage, 206 wmTaskbarCreated uint32 207} 208 209// Loads an image from file and shows it in tray. 210// Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx 211func (t *winTray) setIcon(src string) error { 212 const NIF_ICON = 0x00000002 213 214 h, err := t.loadIconFrom(src) 215 if err != nil { 216 return err 217 } 218 219 t.muNID.Lock() 220 defer t.muNID.Unlock() 221 t.nid.Icon = h 222 t.nid.Flags |= NIF_ICON 223 t.nid.Size = uint32(unsafe.Sizeof(*t.nid)) 224 225 return t.nid.modify() 226} 227 228// Sets tooltip on icon. 229// Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx 230func (t *winTray) setTooltip(src string) error { 231 const NIF_TIP = 0x00000004 232 b, err := windows.UTF16FromString(src) 233 if err != nil { 234 return err 235 } 236 237 t.muNID.Lock() 238 defer t.muNID.Unlock() 239 copy(t.nid.Tip[:], b[:]) 240 t.nid.Flags |= NIF_TIP 241 t.nid.Size = uint32(unsafe.Sizeof(*t.nid)) 242 243 return t.nid.modify() 244} 245 246var wt winTray 247 248// WindowProc callback function that processes messages sent to a window. 249// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633573(v=vs.85).aspx 250func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam uintptr) (lResult uintptr) { 251 const ( 252 WM_RBUTTONUP = 0x0205 253 WM_LBUTTONUP = 0x0202 254 WM_COMMAND = 0x0111 255 WM_ENDSESSION = 0x0016 256 WM_CLOSE = 0x0010 257 WM_DESTROY = 0x0002 258 WM_CREATE = 0x0001 259 ) 260 switch message { 261 case WM_CREATE: 262 systrayReady() 263 case WM_COMMAND: 264 menuItemId := int32(wParam) 265 // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-command#menus 266 if menuItemId != -1 { 267 systrayMenuItemSelected(uint32(wParam)) 268 } 269 case WM_CLOSE: 270 pDestroyWindow.Call(uintptr(t.window)) 271 t.wcex.unregister() 272 case WM_DESTROY: 273 // same as WM_ENDSESSION, but throws 0 exit code after all 274 defer pPostQuitMessage.Call(uintptr(int32(0))) 275 fallthrough 276 case WM_ENDSESSION: 277 t.muNID.Lock() 278 if t.nid != nil { 279 t.nid.delete() 280 } 281 t.muNID.Unlock() 282 systrayExit() 283 case t.wmSystrayMessage: 284 switch lParam { 285 case WM_RBUTTONUP, WM_LBUTTONUP: 286 t.showMenu() 287 } 288 case t.wmTaskbarCreated: // on explorer.exe restarts 289 t.muNID.Lock() 290 t.nid.add() 291 t.muNID.Unlock() 292 default: 293 // Calls the default window procedure to provide default processing for any window messages that an application does not process. 294 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633572(v=vs.85).aspx 295 lResult, _, _ = pDefWindowProc.Call( 296 uintptr(hWnd), 297 uintptr(message), 298 uintptr(wParam), 299 uintptr(lParam), 300 ) 301 } 302 return 303} 304 305func (t *winTray) initInstance() error { 306 const IDI_APPLICATION = 32512 307 const IDC_ARROW = 32512 // Standard arrow 308 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633548(v=vs.85).aspx 309 const SW_HIDE = 0 310 const CW_USEDEFAULT = 0x80000000 311 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms632600(v=vs.85).aspx 312 const ( 313 WS_CAPTION = 0x00C00000 314 WS_MAXIMIZEBOX = 0x00010000 315 WS_MINIMIZEBOX = 0x00020000 316 WS_OVERLAPPED = 0x00000000 317 WS_SYSMENU = 0x00080000 318 WS_THICKFRAME = 0x00040000 319 320 WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX 321 ) 322 // https://msdn.microsoft.com/en-us/library/windows/desktop/ff729176 323 const ( 324 CS_HREDRAW = 0x0002 325 CS_VREDRAW = 0x0001 326 ) 327 const NIF_MESSAGE = 0x00000001 328 329 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644931(v=vs.85).aspx 330 const WM_USER = 0x0400 331 332 const ( 333 className = "SystrayClass" 334 windowName = "" 335 ) 336 337 t.wmSystrayMessage = WM_USER + 1 338 t.visibleItems = make(map[uint32][]uint32) 339 t.menus = make(map[uint32]windows.Handle) 340 t.menuOf = make(map[uint32]windows.Handle) 341 t.menuItemIcons = make(map[uint32]windows.Handle) 342 343 taskbarEventNamePtr, _ := windows.UTF16PtrFromString("TaskbarCreated") 344 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644947 345 res, _, err := pRegisterWindowMessage.Call( 346 uintptr(unsafe.Pointer(taskbarEventNamePtr)), 347 ) 348 t.wmTaskbarCreated = uint32(res) 349 350 t.loadedImages = make(map[string]windows.Handle) 351 352 instanceHandle, _, err := pGetModuleHandle.Call(0) 353 if instanceHandle == 0 { 354 return err 355 } 356 t.instance = windows.Handle(instanceHandle) 357 358 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms648072(v=vs.85).aspx 359 iconHandle, _, err := pLoadIcon.Call(0, uintptr(IDI_APPLICATION)) 360 if iconHandle == 0 { 361 return err 362 } 363 t.icon = windows.Handle(iconHandle) 364 365 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms648391(v=vs.85).aspx 366 cursorHandle, _, err := pLoadCursor.Call(0, uintptr(IDC_ARROW)) 367 if cursorHandle == 0 { 368 return err 369 } 370 t.cursor = windows.Handle(cursorHandle) 371 372 classNamePtr, err := windows.UTF16PtrFromString(className) 373 if err != nil { 374 return err 375 } 376 377 windowNamePtr, err := windows.UTF16PtrFromString(windowName) 378 if err != nil { 379 return err 380 } 381 382 t.wcex = &wndClassEx{ 383 Style: CS_HREDRAW | CS_VREDRAW, 384 WndProc: windows.NewCallback(t.wndProc), 385 Instance: t.instance, 386 Icon: t.icon, 387 Cursor: t.cursor, 388 Background: windows.Handle(6), // (COLOR_WINDOW + 1) 389 ClassName: classNamePtr, 390 IconSm: t.icon, 391 } 392 if err := t.wcex.register(); err != nil { 393 return err 394 } 395 396 windowHandle, _, err := pCreateWindowEx.Call( 397 uintptr(0), 398 uintptr(unsafe.Pointer(classNamePtr)), 399 uintptr(unsafe.Pointer(windowNamePtr)), 400 uintptr(WS_OVERLAPPEDWINDOW), 401 uintptr(CW_USEDEFAULT), 402 uintptr(CW_USEDEFAULT), 403 uintptr(CW_USEDEFAULT), 404 uintptr(CW_USEDEFAULT), 405 uintptr(0), 406 uintptr(0), 407 uintptr(t.instance), 408 uintptr(0), 409 ) 410 if windowHandle == 0 { 411 return err 412 } 413 t.window = windows.Handle(windowHandle) 414 415 pShowWindow.Call( 416 uintptr(t.window), 417 uintptr(SW_HIDE), 418 ) 419 420 pUpdateWindow.Call( 421 uintptr(t.window), 422 ) 423 424 t.muNID.Lock() 425 defer t.muNID.Unlock() 426 t.nid = ¬ifyIconData{ 427 Wnd: windows.Handle(t.window), 428 ID: 100, 429 Flags: NIF_MESSAGE, 430 CallbackMessage: t.wmSystrayMessage, 431 } 432 t.nid.Size = uint32(unsafe.Sizeof(*t.nid)) 433 434 return t.nid.add() 435} 436 437func (t *winTray) createMenu() error { 438 const MIM_APPLYTOSUBMENUS = 0x80000000 // Settings apply to the menu and all of its submenus 439 440 menuHandle, _, err := pCreatePopupMenu.Call() 441 if menuHandle == 0 { 442 return err 443 } 444 t.menus[0] = windows.Handle(menuHandle) 445 446 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647575(v=vs.85).aspx 447 mi := struct { 448 Size, Mask, Style, Max uint32 449 Background windows.Handle 450 ContextHelpID uint32 451 MenuData uintptr 452 }{ 453 Mask: MIM_APPLYTOSUBMENUS, 454 } 455 mi.Size = uint32(unsafe.Sizeof(mi)) 456 457 res, _, err := pSetMenuInfo.Call( 458 uintptr(t.menus[0]), 459 uintptr(unsafe.Pointer(&mi)), 460 ) 461 if res == 0 { 462 return err 463 } 464 return nil 465} 466 467func (t *winTray) convertToSubMenu(menuItemId uint32) (windows.Handle, error) { 468 const MIIM_SUBMENU = 0x00000004 469 470 res, _, err := pCreateMenu.Call() 471 if res == 0 { 472 return 0, err 473 } 474 menu := windows.Handle(res) 475 476 mi := menuItemInfo{Mask: MIIM_SUBMENU, SubMenu: menu} 477 mi.Size = uint32(unsafe.Sizeof(mi)) 478 t.muMenuOf.RLock() 479 hMenu := t.menuOf[menuItemId] 480 t.muMenuOf.RUnlock() 481 res, _, err = pSetMenuItemInfo.Call( 482 uintptr(hMenu), 483 uintptr(menuItemId), 484 0, 485 uintptr(unsafe.Pointer(&mi)), 486 ) 487 if res == 0 { 488 return 0, err 489 } 490 t.muMenus.Lock() 491 t.menus[menuItemId] = menu 492 t.muMenus.Unlock() 493 return menu, nil 494} 495 496func (t *winTray) addOrUpdateMenuItem(menuItemId uint32, parentId uint32, title string, disabled, checked bool) error { 497 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx 498 const ( 499 MIIM_FTYPE = 0x00000100 500 MIIM_BITMAP = 0x00000080 501 MIIM_STRING = 0x00000040 502 MIIM_SUBMENU = 0x00000004 503 MIIM_ID = 0x00000002 504 MIIM_STATE = 0x00000001 505 ) 506 const MFT_STRING = 0x00000000 507 const ( 508 MFS_CHECKED = 0x00000008 509 MFS_DISABLED = 0x00000003 510 ) 511 titlePtr, err := windows.UTF16PtrFromString(title) 512 if err != nil { 513 return err 514 } 515 516 mi := menuItemInfo{ 517 Mask: MIIM_FTYPE | MIIM_STRING | MIIM_ID | MIIM_STATE, 518 Type: MFT_STRING, 519 ID: uint32(menuItemId), 520 TypeData: titlePtr, 521 Cch: uint32(len(title)), 522 } 523 mi.Size = uint32(unsafe.Sizeof(mi)) 524 if disabled { 525 mi.State |= MFS_DISABLED 526 } 527 if checked { 528 mi.State |= MFS_CHECKED 529 } 530 t.muMenuItemIcons.RLock() 531 hIcon := t.menuItemIcons[menuItemId] 532 t.muMenuItemIcons.RUnlock() 533 if hIcon > 0 { 534 mi.Mask |= MIIM_BITMAP 535 mi.BMPItem = hIcon 536 } 537 538 var res uintptr 539 t.muMenus.RLock() 540 menu, exists := t.menus[parentId] 541 t.muMenus.RUnlock() 542 if !exists { 543 menu, err = t.convertToSubMenu(parentId) 544 if err != nil { 545 return err 546 } 547 t.muMenus.Lock() 548 t.menus[parentId] = menu 549 t.muMenus.Unlock() 550 } else if t.getVisibleItemIndex(parentId, menuItemId) != -1 { 551 // We set the menu item info based on the menuID 552 res, _, err = pSetMenuItemInfo.Call( 553 uintptr(menu), 554 uintptr(menuItemId), 555 0, 556 uintptr(unsafe.Pointer(&mi)), 557 ) 558 } 559 560 if res == 0 { 561 t.addToVisibleItems(parentId, menuItemId) 562 position := t.getVisibleItemIndex(parentId, menuItemId) 563 res, _, err = pInsertMenuItem.Call( 564 uintptr(menu), 565 uintptr(position), 566 1, 567 uintptr(unsafe.Pointer(&mi)), 568 ) 569 if res == 0 { 570 t.delFromVisibleItems(parentId, menuItemId) 571 return err 572 } 573 t.muMenuOf.Lock() 574 t.menuOf[menuItemId] = menu 575 t.muMenuOf.Unlock() 576 } 577 578 return nil 579} 580 581func (t *winTray) addSeparatorMenuItem(menuItemId, parentId uint32) error { 582 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx 583 const ( 584 MIIM_FTYPE = 0x00000100 585 MIIM_ID = 0x00000002 586 MIIM_STATE = 0x00000001 587 ) 588 const MFT_SEPARATOR = 0x00000800 589 590 mi := menuItemInfo{ 591 Mask: MIIM_FTYPE | MIIM_ID | MIIM_STATE, 592 Type: MFT_SEPARATOR, 593 ID: uint32(menuItemId), 594 } 595 596 mi.Size = uint32(unsafe.Sizeof(mi)) 597 598 t.addToVisibleItems(parentId, menuItemId) 599 position := t.getVisibleItemIndex(parentId, menuItemId) 600 t.muMenus.RLock() 601 menu := uintptr(t.menus[parentId]) 602 t.muMenus.RUnlock() 603 res, _, err := pInsertMenuItem.Call( 604 menu, 605 uintptr(position), 606 1, 607 uintptr(unsafe.Pointer(&mi)), 608 ) 609 if res == 0 { 610 return err 611 } 612 613 return nil 614} 615 616func (t *winTray) hideMenuItem(menuItemId, parentId uint32) error { 617 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647629(v=vs.85).aspx 618 const MF_BYCOMMAND = 0x00000000 619 const ERROR_SUCCESS syscall.Errno = 0 620 621 t.muMenus.RLock() 622 menu := uintptr(t.menus[parentId]) 623 t.muMenus.RUnlock() 624 res, _, err := pDeleteMenu.Call( 625 menu, 626 uintptr(menuItemId), 627 MF_BYCOMMAND, 628 ) 629 if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS { 630 return err 631 } 632 t.delFromVisibleItems(parentId, menuItemId) 633 634 return nil 635} 636 637func (t *winTray) showMenu() error { 638 const ( 639 TPM_BOTTOMALIGN = 0x0020 640 TPM_LEFTALIGN = 0x0000 641 ) 642 p := point{} 643 res, _, err := pGetCursorPos.Call(uintptr(unsafe.Pointer(&p))) 644 if res == 0 { 645 return err 646 } 647 pSetForegroundWindow.Call(uintptr(t.window)) 648 649 res, _, err = pTrackPopupMenu.Call( 650 uintptr(t.menus[0]), 651 TPM_BOTTOMALIGN|TPM_LEFTALIGN, 652 uintptr(p.X), 653 uintptr(p.Y), 654 0, 655 uintptr(t.window), 656 0, 657 ) 658 if res == 0 { 659 return err 660 } 661 662 return nil 663} 664 665func (t *winTray) delFromVisibleItems(parent, val uint32) { 666 t.muVisibleItems.Lock() 667 defer t.muVisibleItems.Unlock() 668 visibleItems := t.visibleItems[parent] 669 for i, itemval := range visibleItems { 670 if val == itemval { 671 visibleItems = append(visibleItems[:i], visibleItems[i+1:]...) 672 break 673 } 674 } 675} 676 677func (t *winTray) addToVisibleItems(parent, val uint32) { 678 t.muVisibleItems.Lock() 679 defer t.muVisibleItems.Unlock() 680 if visibleItems, exists := t.visibleItems[parent]; !exists { 681 t.visibleItems[parent] = []uint32{val} 682 } else { 683 newvisible := append(visibleItems, val) 684 sort.Slice(newvisible, func(i, j int) bool { return newvisible[i] < newvisible[j] }) 685 t.visibleItems[parent] = newvisible 686 } 687} 688 689func (t *winTray) getVisibleItemIndex(parent, val uint32) int { 690 t.muVisibleItems.RLock() 691 defer t.muVisibleItems.RUnlock() 692 for i, itemval := range t.visibleItems[parent] { 693 if val == itemval { 694 return i 695 } 696 } 697 return -1 698} 699 700// Loads an image from file to be shown in tray or menu item. 701// LoadImage: https://msdn.microsoft.com/en-us/library/windows/desktop/ms648045(v=vs.85).aspx 702func (t *winTray) loadIconFrom(src string) (windows.Handle, error) { 703 const IMAGE_ICON = 1 // Loads an icon 704 const LR_LOADFROMFILE = 0x00000010 // Loads the stand-alone image from the file 705 const LR_DEFAULTSIZE = 0x00000040 // Loads default-size icon for windows(SM_CXICON x SM_CYICON) if cx, cy are set to zero 706 707 // Save and reuse handles of loaded images 708 t.muLoadedImages.RLock() 709 h, ok := t.loadedImages[src] 710 t.muLoadedImages.RUnlock() 711 if !ok { 712 srcPtr, err := windows.UTF16PtrFromString(src) 713 if err != nil { 714 return 0, err 715 } 716 res, _, err := pLoadImage.Call( 717 0, 718 uintptr(unsafe.Pointer(srcPtr)), 719 IMAGE_ICON, 720 0, 721 0, 722 LR_LOADFROMFILE|LR_DEFAULTSIZE, 723 ) 724 if res == 0 { 725 return 0, err 726 } 727 h = windows.Handle(res) 728 t.muLoadedImages.Lock() 729 t.loadedImages[src] = h 730 t.muLoadedImages.Unlock() 731 } 732 return h, nil 733} 734 735func (t *winTray) iconToBitmap(hIcon windows.Handle) (windows.Handle, error) { 736 const SM_CXSMICON = 49 737 const SM_CYSMICON = 50 738 const DI_NORMAL = 0x3 739 hDC, _, err := pGetDC.Call(uintptr(0)) 740 if hDC == 0 { 741 return 0, err 742 } 743 defer pReleaseDC.Call(uintptr(0), hDC) 744 hMemDC, _, err := pCreateCompatibleDC.Call(hDC) 745 if hMemDC == 0 { 746 return 0, err 747 } 748 defer pDeleteDC.Call(hMemDC) 749 cx, _, _ := pGetSystemMetrics.Call(SM_CXSMICON) 750 cy, _, _ := pGetSystemMetrics.Call(SM_CYSMICON) 751 hMemBmp, _, err := pCreateCompatibleBitmap.Call(hDC, cx, cy) 752 if hMemBmp == 0 { 753 return 0, err 754 } 755 hOriginalBmp, _, _ := pSelectObject.Call(hMemDC, hMemBmp) 756 defer pSelectObject.Call(hMemDC, hOriginalBmp) 757 res, _, err := pDrawIconEx.Call(hMemDC, 0, 0, uintptr(hIcon), cx, cy, 0, uintptr(0), DI_NORMAL) 758 if res == 0 { 759 return 0, err 760 } 761 return windows.Handle(hMemBmp), nil 762} 763 764func registerSystray() { 765 if err := wt.initInstance(); err != nil { 766 log.Printf("Unable to init instance: %v", err) 767 return 768 } 769 770 if err := wt.createMenu(); err != nil { 771 log.Printf("Unable to create menu: %v", err) 772 return 773 } 774 775} 776 777func nativeLoop() { 778 // Main message pump. 779 m := &struct { 780 WindowHandle windows.Handle 781 Message uint32 782 Wparam uintptr 783 Lparam uintptr 784 Time uint32 785 Pt point 786 }{} 787 for { 788 ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(m)), 0, 0, 0) 789 790 // If the function retrieves a message other than WM_QUIT, the return value is nonzero. 791 // If the function retrieves the WM_QUIT message, the return value is zero. 792 // If there is an error, the return value is -1 793 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx 794 switch int32(ret) { 795 case -1: 796 log.Printf("Error at message loop: %v", err) 797 return 798 case 0: 799 return 800 default: 801 pTranslateMessage.Call(uintptr(unsafe.Pointer(m))) 802 pDispatchMessage.Call(uintptr(unsafe.Pointer(m))) 803 } 804 } 805} 806 807func quit() { 808 const WM_CLOSE = 0x0010 809 810 pPostMessage.Call( 811 uintptr(wt.window), 812 WM_CLOSE, 813 0, 814 0, 815 ) 816} 817 818func iconBytesToFilePath(iconBytes []byte) (string, error) { 819 bh := md5.Sum(iconBytes) 820 dataHash := hex.EncodeToString(bh[:]) 821 iconFilePath := filepath.Join(os.TempDir(), "systray_temp_icon_"+dataHash) 822 823 if _, err := os.Stat(iconFilePath); os.IsNotExist(err) { 824 if err := ioutil.WriteFile(iconFilePath, iconBytes, 0644); err != nil { 825 return "", err 826 } 827 } 828 return iconFilePath, nil 829} 830 831// SetIcon sets the systray icon. 832// iconBytes should be the content of .ico for windows and .ico/.jpg/.png 833// for other platforms. 834func SetIcon(iconBytes []byte) { 835 iconFilePath, err := iconBytesToFilePath(iconBytes) 836 if err != nil { 837 log.Printf("Unable to write icon data to temp file: %v", err) 838 return 839 } 840 if err := wt.setIcon(iconFilePath); err != nil { 841 log.Printf("Unable to set icon: %v", err) 842 return 843 } 844} 845 846// SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back 847// to a regular icon on other platforms. 848// templateIconBytes and iconBytes should be the content of .ico for windows and 849// .ico/.jpg/.png for other platforms. 850func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { 851 SetIcon(regularIconBytes) 852} 853 854// SetTitle sets the systray title, only available on Mac and Linux. 855func SetTitle(title string) { 856 // do nothing 857} 858 859func (item *MenuItem) parentId() uint32 { 860 if item.parent != nil { 861 return uint32(item.parent.id) 862 } 863 return 0 864} 865 866// SetIcon sets the icon of a menu item. Only works on macOS and Windows. 867// iconBytes should be the content of .ico/.jpg/.png 868func (item *MenuItem) SetIcon(iconBytes []byte) { 869 iconFilePath, err := iconBytesToFilePath(iconBytes) 870 if err != nil { 871 log.Printf("Unable to write icon data to temp file: %v", err) 872 return 873 } 874 875 h, err := wt.loadIconFrom(iconFilePath) 876 if err != nil { 877 log.Printf("Unable to load icon from temp file: %v", err) 878 return 879 } 880 881 h, err = wt.iconToBitmap(h) 882 if err != nil { 883 log.Printf("Unable to convert icon to bitmap: %v", err) 884 return 885 } 886 wt.muMenuItemIcons.Lock() 887 wt.menuItemIcons[uint32(item.id)] = h 888 wt.muMenuItemIcons.Unlock() 889 890 err = wt.addOrUpdateMenuItem(uint32(item.id), item.parentId(), item.title, item.disabled, item.checked) 891 if err != nil { 892 log.Printf("Unable to addOrUpdateMenuItem: %v", err) 893 return 894 } 895} 896 897// SetTooltip sets the systray tooltip to display on mouse hover of the tray icon, 898// only available on Mac and Windows. 899func SetTooltip(tooltip string) { 900 if err := wt.setTooltip(tooltip); err != nil { 901 log.Printf("Unable to set tooltip: %v", err) 902 return 903 } 904} 905 906func addOrUpdateMenuItem(item *MenuItem) { 907 err := wt.addOrUpdateMenuItem(uint32(item.id), item.parentId(), item.title, item.disabled, item.checked) 908 if err != nil { 909 log.Printf("Unable to addOrUpdateMenuItem: %v", err) 910 return 911 } 912} 913 914// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it 915// falls back to the regular icon bytes and on Linux it does nothing. 916// templateIconBytes and regularIconBytes should be the content of .ico for windows and 917// .ico/.jpg/.png for other platforms. 918func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { 919 item.SetIcon(regularIconBytes) 920} 921 922func addSeparator(id uint32) { 923 err := wt.addSeparatorMenuItem(id, 0) 924 if err != nil { 925 log.Printf("Unable to addSeparator: %v", err) 926 return 927 } 928} 929 930func hideMenuItem(item *MenuItem) { 931 err := wt.hideMenuItem(uint32(item.id), item.parentId()) 932 if err != nil { 933 log.Printf("Unable to hideMenuItem: %v", err) 934 return 935 } 936} 937 938func showMenuItem(item *MenuItem) { 939 addOrUpdateMenuItem(item) 940} 941