systray_windows.go 28 KB

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