From d61bb9c79b2d169b2ec221739a8940194e012817 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sat, 27 Jul 2024 16:04:26 +1000 Subject: [PATCH] [Windows] Handle menu accelerators --- v3/examples/menu/main.go | 4 +-- v3/pkg/application/application.go | 18 +++++++++- v3/pkg/application/keys.go | 4 +-- v3/pkg/application/menu_windows.go | 38 +++++++++++--------- v3/pkg/application/menuitem_windows.go | 15 ++++---- v3/pkg/application/popupmenu_windows.go | 31 ++++++++++++++-- v3/pkg/application/webview_window.go | 33 +++++++++++++++-- v3/pkg/application/webview_window_windows.go | 3 +- 8 files changed, 109 insertions(+), 37 deletions(-) diff --git a/v3/examples/menu/main.go b/v3/examples/menu/main.go index 815b3fe62..c8df3016e 100644 --- a/v3/examples/menu/main.go +++ b/v3/examples/menu/main.go @@ -16,7 +16,7 @@ func main() { app := application.New(application.Options{ Name: "Menu Demo", Description: "A demo of the menu system", - //Assets: application.AlphaAssets, + Assets: application.AlphaAssets, Mac: application.MacOptions{ ApplicationShouldTerminateAfterLastWindowClosed: true, }, @@ -35,7 +35,7 @@ func main() { myMenu.Add("Not Enabled").SetEnabled(false) // Click callbacks - myMenu.Add("Click Me!").SetBitmap(clickBitmap).OnClick(func(ctx *application.Context) { + myMenu.Add("Click Me!").SetAccelerator("CmdOrCtrl+l").SetBitmap(clickBitmap).OnClick(func(ctx *application.Context) { switch ctx.ClickedMenuItem().Label() { case "Click Me!": ctx.ClickedMenuItem().SetLabel("Thanks mate!") diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go index 4c5d51f68..c8d9ac998 100644 --- a/v3/pkg/application/application.go +++ b/v3/pkg/application/application.go @@ -311,7 +311,8 @@ type App struct { isDebugMode bool // Keybindings - keyBindings map[string]func(window *WebviewWindow) + keyBindings map[string]func(window *WebviewWindow) + keyBindingsLock sync.RWMutex // Shutdown performingShutdown bool @@ -863,6 +864,9 @@ func (a *App) processKeyBinding(acceleratorString string, window *WebviewWindow) return false } + a.keyBindingsLock.RLock() + defer a.keyBindingsLock.RUnlock() + // Check key bindings callback, ok := a.keyBindings[acceleratorString] if !ok { @@ -875,6 +879,18 @@ func (a *App) processKeyBinding(acceleratorString string, window *WebviewWindow) return true } +func (a *App) addKeyBinding(acceleratorString string, callback func(window *WebviewWindow)) { + a.keyBindingsLock.Lock() + defer a.keyBindingsLock.Unlock() + a.keyBindings[acceleratorString] = callback +} + +func (a *App) removeKeyBinding(acceleratorString string) { + a.keyBindingsLock.Lock() + defer a.keyBindingsLock.Unlock() + delete(a.keyBindings, acceleratorString) +} + func (a *App) handleWindowKeyEvent(event *windowKeyEvent) { // Get window from window map a.windowsLock.RLock() diff --git a/v3/pkg/application/keys.go b/v3/pkg/application/keys.go index c38bf77c2..fce7c94ce 100644 --- a/v3/pkg/application/keys.go +++ b/v3/pkg/application/keys.go @@ -83,9 +83,9 @@ func (a *accelerator) String() string { } slices.Sort(result) if len(a.Key) > 0 { - result = append(result, a.Key) + result = append(result, strings.ToUpper(a.Key)) } - return strings.ToLower(strings.Join(result, "+")) + return strings.Join(result, "+") } var namedKeys = map[string]struct{}{ diff --git a/v3/pkg/application/menu_windows.go b/v3/pkg/application/menu_windows.go index 375914569..e0d3d8a0c 100644 --- a/v3/pkg/application/menu_windows.go +++ b/v3/pkg/application/menu_windows.go @@ -7,7 +7,8 @@ import ( ) type windowsMenu struct { - menu *Menu + menu *Menu + parentWindow *windowsWebviewWindow hWnd w32.HWND hMenu w32.HMENU @@ -36,6 +37,13 @@ func (w *windowsMenu) update() { func (w *windowsMenu) processMenu(parentMenu w32.HMENU, inputMenu *Menu) { for _, item := range inputMenu.items { if item.Hidden() { + if item.accelerator != nil && item.callback != nil { + if w.parentWindow != nil { + w.parentWindow.parent.removeMenuBinding(item.accelerator) + } else { + globalApplication.removeKeyBinding(item.accelerator.String()) + } + } continue } w.currentMenuID++ @@ -52,21 +60,6 @@ func (w *windowsMenu) processMenu(parentMenu w32.HMENU, inputMenu *Menu) { 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 @@ -75,7 +68,18 @@ func (w *windowsMenu) processMenu(parentMenu w32.HMENU, inputMenu *Menu) { itemID = int(newSubmenu) } - var menuText = w32.MustStringToUTF16Ptr(item.Label()) + thisText := item.Label() + if item.accelerator != nil && item.callback != nil { + if w.parentWindow != nil { + w.parentWindow.parent.addMenuBinding(item.accelerator, item) + } else { + globalApplication.addKeyBinding(item.accelerator.String(), func(w *WebviewWindow) { + item.handleClick() + }) + } + thisText = thisText + "\t" + item.accelerator.String() + } + var menuText = w32.MustStringToUTF16Ptr(thisText) w32.AppendMenu(parentMenu, flags, uintptr(itemID), menuText) if item.bitmap != nil { diff --git a/v3/pkg/application/menuitem_windows.go b/v3/pkg/application/menuitem_windows.go index 8342bdb97..691b4ec7d 100644 --- a/v3/pkg/application/menuitem_windows.go +++ b/v3/pkg/application/menuitem_windows.go @@ -336,15 +336,12 @@ func (m *windowsMenuItem) getMenuInfo() *w32.MENUITEMINFO { mii.FType = w32.MFT_SEPARATOR } else { mii.FType = w32.MFT_STRING - //var text string - //if s := a.shortcut; s.Key != 0 { - // text = fmt.Sprintf("%s\t%s", a.text, s.String()) - // shortcut2Action[a.shortcut] = a - //} else { - // text = a.text - //} - mii.DwTypeData = w32.MustStringToUTF16Ptr(m.label) - mii.Cch = uint32(len([]rune(m.label))) + thisText := m.label + if m.menuItem.accelerator != nil { + thisText += "\t" + m.menuItem.accelerator.String() + } + mii.DwTypeData = w32.MustStringToUTF16Ptr(thisText) + mii.Cch = uint32(len([]rune(thisText))) } mii.WID = uint32(m.id) if m.Enabled() { diff --git a/v3/pkg/application/popupmenu_windows.go b/v3/pkg/application/popupmenu_windows.go index 0f5aaa6b3..6d1fa7b89 100644 --- a/v3/pkg/application/popupmenu_windows.go +++ b/v3/pkg/application/popupmenu_windows.go @@ -40,6 +40,7 @@ func (r *RadioGroup) MenuID(item *MenuItem) int { type Win32Menu struct { isPopup bool menu w32.HMENU + parentWindow *windowsWebviewWindow parent w32.HWND menuMapping map[int]*MenuItem checkboxItems map[*MenuItem][]int @@ -61,6 +62,15 @@ func (p *Win32Menu) buildMenu(parentMenu w32.HMENU, inputMenu *Menu) { var currentRadioGroup RadioGroup for _, item := range inputMenu.items { if item.Hidden() { + if item.accelerator != nil { + if p.parentWindow != nil { + // Remove the accelerator from the keybindings + p.parentWindow.parent.removeMenuBinding(item.accelerator) + } else { + // Remove the global keybindings + globalApplication.removeKeyBinding(item.accelerator.String()) + } + } continue } p.currentMenuID++ @@ -85,7 +95,9 @@ func (p *Win32Menu) buildMenu(parentMenu w32.HMENU, inputMenu *Menu) { p.checkboxItems[item] = append(p.checkboxItems[item], itemID) } if item.IsRadio() { - currentRadioGroup.Add(itemID, item) + if currentRadioGroup != nil { + currentRadioGroup.Add(itemID, item) + } } else { if len(currentRadioGroup) > 0 { for _, radioMember := range currentRadioGroup { @@ -105,6 +117,18 @@ func (p *Win32Menu) buildMenu(parentMenu w32.HMENU, inputMenu *Menu) { } var menuText = item.Label() + if item.accelerator != nil { + menuText = menuText + "\t" + item.accelerator.String() + if item.callback != nil { + if p.parentWindow != nil { + p.parentWindow.parent.addMenuBinding(item.accelerator, item) + } else { + globalApplication.addKeyBinding(item.accelerator.String(), func(w *WebviewWindow) { + item.handleClick() + }) + } + } + } ok := w32.AppendMenu(parentMenu, flags, uintptr(itemID), w32.MustStringToUTF16Ptr(menuText)) if !ok { w32.Fatal(fmt.Sprintf("Error adding menu item: %s", menuText)) @@ -146,9 +170,10 @@ func NewPopupMenu(parent w32.HWND, inputMenu *Menu) *Win32Menu { result.Update() return result } -func NewApplicationMenu(parent w32.HWND, inputMenu *Menu) *Win32Menu { +func NewApplicationMenu(parent *windowsWebviewWindow, inputMenu *Menu) *Win32Menu { result := &Win32Menu{ - parent: parent, + parentWindow: parent, + parent: parent.hwnd, menuData: inputMenu, checkboxItems: make(map[*MenuItem][]int), radioGroups: make(map[*MenuItem][]*RadioGroup), diff --git a/v3/pkg/application/webview_window.go b/v3/pkg/application/webview_window.go index de1421510..3ecb19f90 100644 --- a/v3/pkg/application/webview_window.go +++ b/v3/pkg/application/webview_window.go @@ -132,7 +132,12 @@ type WebviewWindow struct { cancellers []func() // keyBindings holds the keybindings for the window - keyBindings map[string]func(window *WebviewWindow) + keyBindings map[string]func(*WebviewWindow) + keyBindingsLock sync.RWMutex + + // menuBindings holds the menu bindings for the window + menuBindings map[string]*MenuItem + menuBindingsLock sync.RWMutex // Indicates that the window is destroyed destroyed bool @@ -211,6 +216,7 @@ func NewWindow(options WebviewWindowOptions) *WebviewWindow { eventListeners: make(map[uint][]*WindowEventListener), contextMenus: make(map[string]*Menu), eventHooks: make(map[uint][]*WindowEventListener), + menuBindings: make(map[string]*MenuItem), } result.setupEventMapping() @@ -1176,12 +1182,23 @@ func (w *WebviewWindow) SetPosition(x int, y int) { } func (w *WebviewWindow) processKeyBinding(acceleratorString string) bool { + // Check menu bindings + if w.menuBindings != nil { + w.menuBindingsLock.RLock() + defer w.menuBindingsLock.RUnlock() + if menuItem := w.menuBindings[acceleratorString]; menuItem != nil { + menuItem.handleClick() + return true + } + } + // Check key bindings if w.keyBindings != nil { + w.keyBindingsLock.RLock() + defer w.keyBindingsLock.RUnlock() if callback := w.keyBindings[acceleratorString]; callback != nil { // Execute callback go callback(w) - return true } } @@ -1203,3 +1220,15 @@ func (w *WebviewWindow) isDestroyed() bool { defer w.destroyedLock.RUnlock() return w.destroyed } + +func (w *WebviewWindow) removeMenuBinding(a *accelerator) { + w.menuBindingsLock.Lock() + defer w.menuBindingsLock.Unlock() + w.menuBindings[a.String()] = nil +} + +func (w *WebviewWindow) addMenuBinding(a *accelerator, menuItem *MenuItem) { + w.menuBindingsLock.Lock() + defer w.menuBindingsLock.Unlock() + w.menuBindings[a.String()] = menuItem +} diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index 790c8ef70..771ebbeee 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -234,7 +234,8 @@ func (w *windowsWebviewWindow) run() { theMenu = w.parent.options.Windows.Menu } if theMenu != nil { - w.menu = NewApplicationMenu(w.hwnd, theMenu) + w.menu = NewApplicationMenu(w, theMenu) + w.menu.parentWindow = w appMenu = w.menu.menu } }