From 7fd627f1694bbbc48ed4844b5f3ab9185396598b Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Wed, 10 May 2023 19:35:40 +1000 Subject: [PATCH] [v3 windows] initial systray support --- v3/examples/systray/main.go | 6 +- v3/pkg/application/application_windows.go | 2 +- v3/pkg/application/menu_windows.go | 125 +++++++++++++----- v3/pkg/application/menuitem.go | 30 +++++ v3/pkg/application/systemtray_windows.go | 40 +++--- v3/pkg/w32/constants.go | 16 ++- .../win32/menu.go => v3/pkg/w32/popupmenu.go | 52 ++++---- v3/pkg/w32/user32.go | 106 ++++++++------- 8 files changed, 241 insertions(+), 136 deletions(-) rename v2/internal/platform/win32/menu.go => v3/pkg/w32/popupmenu.go (62%) diff --git a/v3/examples/systray/main.go b/v3/examples/systray/main.go index a8a27f878..cbc002af5 100644 --- a/v3/examples/systray/main.go +++ b/v3/examples/systray/main.go @@ -35,11 +35,13 @@ func main() { myMenu := app.NewMenu() myMenu.Add("Hello World!").OnClick(func(ctx *application.Context) { - app.InfoDialog().SetTitle("Hello World!").SetMessage("Hello World!").Show() + println("Hello World!") + // app.InfoDialog().SetTitle("Hello World!").SetMessage("Hello World!").Show() }) subMenu := myMenu.AddSubmenu("Submenu") subMenu.Add("Click me!").OnClick(func(ctx *application.Context) { - ctx.ClickedMenuItem().SetLabel("Clicked!") + println("Click me!") + // ctx.ClickedMenuItem().SetLabel("Clicked!") }) myMenu.AddSeparator() myMenu.Add("Quit").OnClick(func(ctx *application.Context) { diff --git a/v3/pkg/application/application_windows.go b/v3/pkg/application/application_windows.go index 159952789..77d614b60 100644 --- a/v3/pkg/application/application_windows.go +++ b/v3/pkg/application/application_windows.go @@ -96,7 +96,7 @@ func (m *windowsApp) getCurrentWindowID() uint { func (m *windowsApp) setApplicationMenu(menu *Menu) { if menu == nil { - // Create a default menu for mac + // Create a default menu for windows menu = defaultApplicationMenu() } menu.Update() diff --git a/v3/pkg/application/menu_windows.go b/v3/pkg/application/menu_windows.go index 33cd608c2..efa1bd0ee 100644 --- a/v3/pkg/application/menu_windows.go +++ b/v3/pkg/application/menu_windows.go @@ -2,53 +2,112 @@ package application -import "unsafe" +import ( + "github.com/wailsapp/wails/v3/pkg/w32" +) type windowsMenu struct { menu *Menu - menuImpl unsafe.Pointer + hWnd w32.HWND + hMenu w32.HMENU + currentMenuID int + menuMapping map[int]*MenuItem + checkboxItems []*Menu } func newMenuImpl(menu *Menu) *windowsMenu { result := &windowsMenu{ - menu: menu, + menu: menu, + menuMapping: make(map[int]*MenuItem), } + return result } -func (m *windowsMenu) update() { - //if m.menuImpl == nil { - // m.menuImpl = C.createNSMenu(C.CString(m.menu.label)) - //} else { - // C.clearMenu(m.menuImpl) - //} - m.processMenu(m.menuImpl, m.menu) +func (w *windowsMenu) update() { + if w.hMenu != 0 { + w32.DestroyMenu(w.hMenu) + } + w.hMenu = w32.NewPopupMenu() + w.processMenu(w.hMenu, w.menu) } -func (m *windowsMenu) processMenu(parent unsafe.Pointer, menu *Menu) { - //for _, item := range menu.items { - // switch item.itemType { - // case submenu: - // submenu := item.submenu - // nsSubmenu := C.createNSMenu(C.CString(item.label)) - // m.processMenu(nsSubmenu, submenu) - // menuItem := newMenuItemImpl(item) - // item.impl = menuItem - // C.addMenuItem(parent, menuItem.nsMenuItem) - // C.setMenuItemSubmenu(menuItem.nsMenuItem, nsSubmenu) - // if item.role == ServicesMenu { - // C.addServicesMenu(nsSubmenu) - // } - // case text, checkbox, radio: - // menuItem := newMenuItemImpl(item) - // item.impl = menuItem - // C.addMenuItem(parent, menuItem.nsMenuItem) - // case separator: - // C.addMenuSeparator(parent) - // } - // - //} +func (w *windowsMenu) processMenu(parentMenu w32.HMENU, inputMenu *Menu) { + for _, item := range inputMenu.items { + if item.Hidden() { + continue + } + w.currentMenuID++ + itemID := w.currentMenuID + w.menuMapping[itemID] = item + + flags := uint32(w32.MF_STRING) + if item.disabled { + flags = flags | w32.MF_GRAYED + } + if item.checked { + flags = flags | w32.MF_CHECKED + } + if item.IsSeparator() { + flags = flags | w32.MF_SEPARATOR + } + // + //if item.IsCheckbox() { + // w.checkboxItems[item] = append(w.checkboxItems[item], itemID) + //} + //if item.IsRadio() { + // currentRadioGroup.Add(itemID, item) + //} else { + // if len(currentRadioGroup) > 0 { + // for _, radioMember := range currentRadioGroup { + // currentRadioGroup := currentRadioGroup + // p.radioGroups[radioMember.MenuItem] = append(p.radioGroups[radioMember.MenuItem], ¤tRadioGroup) + // } + // currentRadioGroup = RadioGroup{} + // } + //} + + if item.submenu != nil { + flags = flags | w32.MF_POPUP + newSubmenu := w32.CreateMenu() + w.processMenu(newSubmenu, item.submenu) + itemID = int(newSubmenu) + } + + var menuText = w32.MustStringToUTF16Ptr(item.Label()) + + w32.AppendMenu(parentMenu, flags, uintptr(itemID), menuText) + } +} + +func (w *windowsMenu) ShowAtCursor() { + invokeSync(func() { + x, y, ok := w32.GetCursorPos() + if !ok { + return + } + w.ShowAt(x, y) + }) +} + +func (w *windowsMenu) ShowAt(x int, y int) { + w.update() + w32.TrackPopupMenuEx(w.hMenu, + w32.TPM_LEFTALIGN, + int32(x), + int32(y), + w.hWnd, + nil) + w32.PostMessage(w.hWnd, w32.WM_NULL, 0, 0) +} + +func (w *windowsMenu) ProcessCommand(cmdMsgID int) { + item := w.menuMapping[cmdMsgID] + if item == nil { + return + } + item.handleClick() } func defaultApplicationMenu() *Menu { diff --git a/v3/pkg/application/menuitem.go b/v3/pkg/application/menuitem.go index ed7143e29..e30750781 100644 --- a/v3/pkg/application/menuitem.go +++ b/v3/pkg/application/menuitem.go @@ -38,6 +38,7 @@ type menuItemImpl interface { setDisabled(disabled bool) setChecked(checked bool) setAccelerator(accelerator *accelerator) + setHidden(hidden bool) } type MenuItem struct { @@ -46,6 +47,7 @@ type MenuItem struct { tooltip string disabled bool checked bool + hidden bool submenu *Menu callback func(*Context) itemType menuItemType @@ -257,10 +259,38 @@ func (m *MenuItem) SetChecked(checked bool) *MenuItem { return m } +func (m *MenuItem) SetHidden(hidden bool) *MenuItem { + m.hidden = hidden + if m.impl != nil { + m.impl.setHidden(m.hidden) + } + return m +} + func (m *MenuItem) Checked() bool { return m.checked } +func (m *MenuItem) IsSeparator() bool { + return m.itemType == separator +} + +func (m *MenuItem) IsSubmenu() bool { + return m.itemType == submenu +} + +func (m *MenuItem) IsCheckbox() bool { + return m.itemType == checkbox +} + +func (m *MenuItem) IsRadio() bool { + return m.itemType == radio +} + +func (m *MenuItem) Hidden() bool { + return m.hidden +} + func (m *MenuItem) OnClick(f func(*Context)) *MenuItem { m.callback = f return m diff --git a/v3/pkg/application/systemtray_windows.go b/v3/pkg/application/systemtray_windows.go index 58308fbd0..8e053e5f7 100644 --- a/v3/pkg/application/systemtray_windows.go +++ b/v3/pkg/application/systemtray_windows.go @@ -19,13 +19,14 @@ const ( type windowsSystemTray struct { parent *SystemTray + menu *windowsMenu + // Platform specific implementation uid uint32 hwnd w32.HWND lightModeIcon w32.HICON darkModeIcon w32.HICON currentIcon w32.HICON - //menu *w32.PopupMenu } func (s *windowsSystemTray) setMenu(menu *Menu) { @@ -82,7 +83,9 @@ func (s *windowsSystemTray) run() { } s.uid = nid.UID - // TODO: Set Menu + if s.parent.menu != nil { + s.updateMenu() + } // Set Default Callbacks if s.parent.clickHandler == nil { @@ -92,7 +95,9 @@ func (s *windowsSystemTray) run() { } if s.parent.rightClickHandler == nil { s.parent.rightClickHandler = func() { - //s.showMenu() + if s.menu != nil { + s.menu.ShowAtCursor() + } } } @@ -174,10 +179,6 @@ func newSystemTrayImpl(parent *SystemTray) systemTrayImpl { } } -func (s *windowsSystemTray) destroy() { - panic("implement me") -} - func (s *windowsSystemTray) wndProc(msg uint32, wParam, lParam uintptr) uintptr { switch msg { case WM_USER_SYSTRAY: @@ -211,20 +212,26 @@ func (s *windowsSystemTray) wndProc(msg uint32, wParam, lParam uintptr) uintptr //println(w32.WMMessageToString(msg)) // TODO: Menu processing - //case w32.WM_COMMAND: - // cmdMsgID := int(wparam & 0xffff) - // switch cmdMsgID { - // default: - // p.menu.ProcessCommand(cmdMsgID) - // } + case w32.WM_COMMAND: + cmdMsgID := int(wParam & 0xffff) + switch cmdMsgID { + default: + s.menu.ProcessCommand(cmdMsgID) + } default: - msg := int(wParam & 0xffff) - println(w32.WMMessageToString(uintptr(msg))) + //msg := int(wParam & 0xffff) + //println(w32.WMMessageToString(uintptr(msg))) } return w32.DefWindowProc(s.hwnd, msg, wParam, lParam) } +func (s *windowsSystemTray) updateMenu() { + s.menu = newMenuImpl(s.parent.menu) + s.menu.hWnd = s.hwnd + s.menu.update() +} + // ---- Unsupported ---- func (s *windowsSystemTray) setLabel(_ string) { @@ -238,3 +245,6 @@ func (s *windowsSystemTray) setTemplateIcon(_ []byte) { func (s *windowsSystemTray) setIconPosition(position int) { // Unsupported - do nothing } + +func (s *windowsSystemTray) destroy() { +} diff --git a/v3/pkg/w32/constants.go b/v3/pkg/w32/constants.go index 8fbcedf3e..46e834ee9 100644 --- a/v3/pkg/w32/constants.go +++ b/v3/pkg/w32/constants.go @@ -3360,8 +3360,20 @@ const ( ) const ( - MF_BYCOMMAND = 0x00000000 - MF_BYPOSITION = 0x00000400 + MF_BYCOMMAND = 0x00000000 + MF_BYPOSITION = 0x00000400 + MF_BITMAP = 0x00000004 + MF_CHECKED = 0x00000008 + MF_DISABLED = 0x00000002 + MF_ENABLED = 0x00000000 + MF_GRAYED = 0x00000001 + MF_MENUBARBREAK = 0x00000020 + MF_MENUBREAK = 0x00000040 + MF_OWNERDRAW = 0x00000100 + MF_POPUP = 0x00000010 + MF_SEPARATOR = 0x00000800 + MF_STRING = 0x00000000 + MF_UNCHECKED = 0x00000000 ) type MENUITEMINFO struct { diff --git a/v2/internal/platform/win32/menu.go b/v3/pkg/w32/popupmenu.go similarity index 62% rename from v2/internal/platform/win32/menu.go rename to v3/pkg/w32/popupmenu.go index f05886414..267b3af44 100644 --- a/v2/internal/platform/win32/menu.go +++ b/v3/pkg/w32/popupmenu.go @@ -1,15 +1,8 @@ -//go:build windows - -package win32 +package w32 type Menu HMENU type PopupMenu Menu -func CreatePopupMenu() PopupMenu { - ret, _, _ := procCreatePopupMenu.Call(0, 0, 0, 0) - return PopupMenu(ret) -} - func (m Menu) Destroy() bool { ret, _, _ := procDestroyMenu.Call(uintptr(m)) return ret != 0 @@ -19,31 +12,22 @@ func (p PopupMenu) Destroy() bool { return Menu(p).Destroy() } -func (p PopupMenu) Track(flags uint, x, y int, wnd HWND) bool { - ret, _, _ := procTrackPopupMenu.Call( - uintptr(p), - uintptr(flags), - uintptr(x), - uintptr(y), - 0, - uintptr(wnd), - 0, - ) - return ret != 0 +func (p PopupMenu) Track(hwnd HWND, flags uint32, x, y int32) bool { + return TrackPopupMenuEx( + HMENU(p), + flags, + x, + y, + hwnd, + nil) } -func (p PopupMenu) Append(flags uintptr, id uintptr, text string) bool { +func (p PopupMenu) Append(flags uint32, id uintptr, text string) bool { return Menu(p).Append(flags, id, text) } -func (m Menu) Append(flags uintptr, id uintptr, text string) bool { - ret, _, _ := procAppendMenuW.Call( - uintptr(m), - flags, - id, - MustStringToUTF16uintptr(text), - ) - return ret != 0 +func (m Menu) Append(flags uint32, id uintptr, text string) bool { + return AppendMenu(HMENU(m), flags, id, MustStringToUTF16Ptr(text)) } func (p PopupMenu) Check(id uintptr, checked bool) bool { @@ -70,7 +54,7 @@ func (m Menu) CheckRadio(startID int, endID int, selectedID int) bool { func CheckMenuItem(menu HMENU, id uintptr, flags uint) uint { ret, _, _ := procCheckMenuItem.Call( - uintptr(menu), + menu, id, uintptr(flags), ) @@ -80,3 +64,13 @@ func CheckMenuItem(menu HMENU, id uintptr, flags uint) uint { func (p PopupMenu) CheckRadio(startID, endID, selectedID int) bool { return Menu(p).CheckRadio(startID, endID, selectedID) } + +func NewMenu() HMENU { + ret, _, _ := procCreateMenu.Call() + return HMENU(ret) +} + +func NewPopupMenu() HMENU { + ret, _, _ := procCreatePopupMenu.Call() + return ret +} diff --git a/v3/pkg/w32/user32.go b/v3/pkg/w32/user32.go index 724ad5319..bb293bc3f 100644 --- a/v3/pkg/w32/user32.go +++ b/v3/pkg/w32/user32.go @@ -21,6 +21,7 @@ var ( procLoadIcon = moduser32.NewProc("LoadIconW") procLoadCursor = moduser32.NewProc("LoadCursorW") procShowWindow = moduser32.NewProc("ShowWindow") + procGetDesktopWindow = moduser32.NewProc("GetDesktopWindow") procShowWindowAsync = moduser32.NewProc("ShowWindowAsync") procUpdateWindow = moduser32.NewProc("UpdateWindow") procCreateWindowEx = moduser32.NewProc("CreateWindowExW") @@ -89,11 +90,8 @@ var ( procCreatePopupMenu = moduser32.NewProc("CreatePopupMenu") procCheckMenuRadioItem = moduser32.NewProc("CheckMenuRadioItem") procCreateIconFromResourceEx = moduser32.NewProc("CreateIconFromResourceEx") - //procSysColorBrush = moduser32.NewProc("GetSysColorBrush") - //procSetMenu = moduser32.NewProc("SetMenu") - //procDrawMenuBar = moduser32.NewProc("DrawMenuBar") - //procInsertMenuItem = moduser32.NewProc("InsertMenuItemW") // FIXIT: - + procInsertMenuItem = moduser32.NewProc("InsertMenuItemW") + procCheckMenuItem = moduser32.NewProc("CheckMenuItem") procClientToScreen = moduser32.NewProc("ClientToScreen") procIsDialogMessage = moduser32.NewProc("IsDialogMessageW") procIsWindow = moduser32.NewProc("IsWindow") @@ -146,20 +144,19 @@ var ( procSetClassLong = moduser32.NewProc("SetClassLongW") procSetClassLongPtr = moduser32.NewProc("SetClassLongPtrW") - libuser32, _ = syscall.LoadLibrary("user32.dll") - insertMenuItem, _ = syscall.GetProcAddress(libuser32, "InsertMenuItemW") - setMenuItemInfo, _ = syscall.GetProcAddress(libuser32, "SetMenuItemInfoW") - setMenu, _ = syscall.GetProcAddress(libuser32, "SetMenu") - drawMenuBar, _ = syscall.GetProcAddress(libuser32, "DrawMenuBar") - trackPopupMenuEx, _ = syscall.GetProcAddress(libuser32, "TrackPopupMenuEx") - getKeyState, _ = syscall.GetProcAddress(libuser32, "GetKeyState") - getSysColorBrush, _ = syscall.GetProcAddress(libuser32, "GetSysColorBrush") + procSetMenu = moduser32.NewProc("SetMenu") + procAppendMenu = moduser32.NewProc("AppendMenuW") + procSetMenuItemInfo = moduser32.NewProc("SetMenuItemInfoW") + procDrawMenuBar = moduser32.NewProc("DrawMenuBar") + procTrackPopupMenuEx = moduser32.NewProc("TrackPopupMenuEx") + procGetKeyState = moduser32.NewProc("GetKeyState") + procGetSysColorBrush = moduser32.NewProc("GetSysColorBrush") - getWindowPlacement, _ = syscall.GetProcAddress(libuser32, "GetWindowPlacement") - setWindowPlacement, _ = syscall.GetProcAddress(libuser32, "SetWindowPlacement") + procGetWindowPlacement = moduser32.NewProc("GetWindowPlacement") + procSetWindowPlacement = moduser32.NewProc("SetWindowPlacement") - setScrollInfo, _ = syscall.GetProcAddress(libuser32, "SetScrollInfo") - getScrollInfo, _ = syscall.GetProcAddress(libuser32, "GetScrollInfo") + procGetScrollInfo = moduser32.NewProc("GetScrollInfo") + procSetScrollInfo = moduser32.NewProc("SetScrollInfo") mainThread HANDLE ) @@ -182,6 +179,11 @@ func RegisterClassEx(wndClassEx *WNDCLASSEX) ATOM { return ATOM(ret) } +func GetDesktopWindow() HWND { + ret, _, _ := procGetDesktopWindow.Call() + return ret +} + func LoadIcon(instance HINSTANCE, iconName *uint16) HICON { ret, _, _ := procLoadIcon.Call( uintptr(instance), @@ -667,15 +669,7 @@ func GetSystemMetrics(index int) int { } func GetSysColorBrush(nIndex int) HBRUSH { - /* - ret, _, _ := procSysColorBrush.Call(1, - uintptr(nIndex), - 0, - 0) - - return HBRUSH(ret) - */ - ret, _, _ := syscall.Syscall(getSysColorBrush, 1, + ret, _, _ := procGetSysColorBrush.Call(1, uintptr(nIndex), 0, 0) @@ -828,10 +822,17 @@ func CreateMenu() HMENU { } func SetMenu(hWnd HWND, hMenu HMENU) bool { - ret, _, _ := syscall.Syscall(setMenu, 2, - uintptr(hWnd), - uintptr(hMenu), - 0) + + ret, _, _ := procSetMenu.Call(hWnd, hMenu) + return ret != 0 +} + +func AppendMenu(hMenu HMENU, uFlags uint32, uIDNewItem uintptr, lpNewItem *uint16) bool { + ret, _, _ := procAppendMenu.Call( + hMenu, + uintptr(uFlags), + uIDNewItem, + uintptr(unsafe.Pointer(lpNewItem))) return ret != 0 } @@ -848,39 +849,36 @@ func SelectRadioMenuItem(menuID uint16, startID uint16, endID uint16, hwnd HWND) } -func CreatePopupMenu() HMENU { +func CreatePopupMenu() PopupMenu { ret, _, _ := procCreatePopupMenu.Call(0, 0, 0, 0) - return HMENU(ret) + return PopupMenu(ret) } -func TrackPopupMenuEx(hMenu HMENU, fuFlags uint32, x, y int32, hWnd HWND, lptpm *TPMPARAMS) BOOL { - ret, _, _ := syscall.Syscall6(trackPopupMenuEx, 6, - uintptr(hMenu), +func TrackPopupMenuEx(hMenu HMENU, fuFlags uint32, x, y int32, hWnd HWND, lptpm *TPMPARAMS) bool { + + ret, _, _ := procTrackPopupMenuEx.Call( + hMenu, uintptr(fuFlags), uintptr(x), uintptr(y), - uintptr(hWnd), + hWnd, uintptr(unsafe.Pointer(lptpm))) - return BOOL(ret) -} - -func DrawMenuBar(hWnd HWND) bool { - ret, _, _ := syscall.Syscall(drawMenuBar, 1, - uintptr(hWnd), - 0, - 0) - + return ret != 0 +} + +func DrawMenuBar(hWnd HWND) bool { + ret, _, _ := procDrawMenuBar.Call(hWnd, 0, 0) return ret != 0 } func InsertMenuItem(hMenu HMENU, uItem uint32, fByPosition bool, lpmii *MENUITEMINFO) bool { - ret, _, _ := syscall.Syscall6(insertMenuItem, 4, - uintptr(hMenu), + ret, _, _ := procInsertMenuItem.Call( + hMenu, uintptr(uItem), uintptr(BoolToBOOL(fByPosition)), uintptr(unsafe.Pointer(lpmii)), @@ -891,8 +889,8 @@ func InsertMenuItem(hMenu HMENU, uItem uint32, fByPosition bool, lpmii *MENUITEM } func SetMenuItemInfo(hMenu HMENU, uItem uint32, fByPosition bool, lpmii *MENUITEMINFO) bool { - ret, _, _ := syscall.Syscall6(setMenuItemInfo, 4, - uintptr(hMenu), + ret, _, _ := procSetMenuItemInfo.Call( + hMenu, uintptr(uItem), uintptr(BoolToBOOL(fByPosition)), uintptr(unsafe.Pointer(lpmii)), @@ -1273,7 +1271,7 @@ func CallNextHookEx(hhk HHOOK, nCode int, wParam WPARAM, lParam LPARAM) LRESULT } func GetKeyState(nVirtKey int32) int16 { - ret, _, _ := syscall.Syscall(getKeyState, 1, + ret, _, _ := procGetKeyState.Call( uintptr(nVirtKey), 0, 0) @@ -1291,7 +1289,7 @@ func DestroyMenu(hMenu HMENU) bool { } func GetWindowPlacement(hWnd HWND, lpwndpl *WINDOWPLACEMENT) bool { - ret, _, _ := syscall.Syscall(getWindowPlacement, 2, + ret, _, _ := procGetWindowPlacement.Call( uintptr(hWnd), uintptr(unsafe.Pointer(lpwndpl)), 0) @@ -1300,7 +1298,7 @@ func GetWindowPlacement(hWnd HWND, lpwndpl *WINDOWPLACEMENT) bool { } func SetWindowPlacement(hWnd HWND, lpwndpl *WINDOWPLACEMENT) bool { - ret, _, _ := syscall.Syscall(setWindowPlacement, 2, + ret, _, _ := procSetWindowPlacement.Call( uintptr(hWnd), uintptr(unsafe.Pointer(lpwndpl)), 0) @@ -1309,7 +1307,7 @@ func SetWindowPlacement(hWnd HWND, lpwndpl *WINDOWPLACEMENT) bool { } func SetScrollInfo(hwnd HWND, fnBar int32, lpsi *SCROLLINFO, fRedraw bool) int32 { - ret, _, _ := syscall.Syscall6(setScrollInfo, 4, + ret, _, _ := procSetScrollInfo.Call( hwnd, uintptr(fnBar), uintptr(unsafe.Pointer(lpsi)), @@ -1321,7 +1319,7 @@ func SetScrollInfo(hwnd HWND, fnBar int32, lpsi *SCROLLINFO, fRedraw bool) int32 } func GetScrollInfo(hwnd HWND, fnBar int32, lpsi *SCROLLINFO) bool { - ret, _, _ := syscall.Syscall(getScrollInfo, 3, + ret, _, _ := procGetScrollInfo.Call( hwnd, uintptr(fnBar), uintptr(unsafe.Pointer(lpsi)))