From e424a85a9978ef42c50e217ef9250c09a75f796d Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Mon, 20 May 2024 21:15:02 +1000 Subject: [PATCH] Menu improvements (#3492) * Expose `DefaultApplicationMenu`. Add `FindByLabel` and `ItemAt` for finding menu items in a menu * Add `Menu.RemoveMenuItem()`, `MneuItem.GetAccelerator()` and `MenuItem.RemoveAccelerator()` * Remove `Update` * Iterate when removing menu items * Add `GetSubmenu()` --- v3/pkg/application/application_darwin.go | 2 +- v3/pkg/application/application_linux.go | 2 +- v3/pkg/application/application_windows.go | 2 +- v3/pkg/application/menu.go | 68 ++++++++-- v3/pkg/application/menu_darwin.go | 2 +- v3/pkg/application/menu_linux.go | 2 +- v3/pkg/application/menu_test.go | 140 +++++++++++++++++++++ v3/pkg/application/menu_windows.go | 2 +- v3/pkg/application/menuitem.go | 41 ++++-- v3/pkg/application/menuitem_darwin.go | 48 +++---- v3/pkg/application/menuitem_dev.go | 2 +- v3/pkg/application/menuitem_linux.go | 48 +++---- v3/pkg/application/menuitem_test.go | 61 +++++++++ v3/pkg/application/menuitem_windows.go | 24 ++-- v3/pkg/application/roles.go | 12 +- v3/pkg/application/webview_window_linux.go | 2 +- 16 files changed, 364 insertions(+), 94 deletions(-) create mode 100644 v3/pkg/application/menu_test.go create mode 100644 v3/pkg/application/menuitem_test.go diff --git a/v3/pkg/application/application_darwin.go b/v3/pkg/application/application_darwin.go index 68ae516bf..c66f5b129 100644 --- a/v3/pkg/application/application_darwin.go +++ b/v3/pkg/application/application_darwin.go @@ -210,7 +210,7 @@ func (m *macosApp) getCurrentWindowID() uint { func (m *macosApp) setApplicationMenu(menu *Menu) { if menu == nil { // Create a default menu for mac - menu = defaultApplicationMenu() + menu = DefaultApplicationMenu() } menu.Update() diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go index 22fd6da5c..e7d681692 100644 --- a/v3/pkg/application/application_linux.go +++ b/v3/pkg/application/application_linux.go @@ -85,7 +85,7 @@ func (a *linuxApp) setApplicationMenu(menu *Menu) { // FIXME: How do we avoid putting a menu? if menu == nil { // Create a default menu - menu = defaultApplicationMenu() + menu = DefaultApplicationMenu() globalApplication.ApplicationMenu = menu } } diff --git a/v3/pkg/application/application_windows.go b/v3/pkg/application/application_windows.go index 430852e27..4c482e57c 100644 --- a/v3/pkg/application/application_windows.go +++ b/v3/pkg/application/application_windows.go @@ -169,7 +169,7 @@ func (m *windowsApp) getCurrentWindowID() uint { func (m *windowsApp) setApplicationMenu(menu *Menu) { if menu == nil { // Create a default menu for windows - menu = defaultApplicationMenu() + menu = DefaultApplicationMenu() } menu.Update() diff --git a/v3/pkg/application/menu.go b/v3/pkg/application/menu.go index ae75c00ad..ba2c60c21 100644 --- a/v3/pkg/application/menu.go +++ b/v3/pkg/application/menu.go @@ -16,24 +16,24 @@ func NewMenu() *Menu { } func (m *Menu) Add(label string) *MenuItem { - result := newMenuItem(label) + result := NewMenuItem(label) m.items = append(m.items, result) return result } func (m *Menu) AddSeparator() { - result := newMenuItemSeparator() + result := NewMenuItemSeparator() m.items = append(m.items, result) } func (m *Menu) AddCheckbox(label string, enabled bool) *MenuItem { - result := newMenuItemCheckbox(label, enabled) + result := NewMenuItemCheckbox(label, enabled) m.items = append(m.items, result) return result } func (m *Menu) AddRadio(label string, enabled bool) *MenuItem { - result := newMenuItemRadio(label, enabled) + result := NewMenuItemRadio(label, enabled) m.items = append(m.items, result) return result } @@ -47,13 +47,13 @@ func (m *Menu) Update() { } func (m *Menu) AddSubmenu(s string) *Menu { - result := newSubMenuItem(s) + result := NewSubMenuItem(s) m.items = append(m.items, result) return result.submenu } func (m *Menu) AddRole(role Role) *Menu { - result := newRole(role) + result := NewRole(role) if result != nil { m.items = append(m.items, result) } @@ -95,13 +95,51 @@ func (m *Menu) setContextData(data *ContextMenuData) { } } +// FindByLabel recursively searches for a menu item with the given label +// and returns the first match, or nil if not found. +func (m *Menu) FindByLabel(label string) *MenuItem { + for _, item := range m.items { + if item.label == label { + return item + } + if item.submenu != nil { + found := item.submenu.FindByLabel(label) + if found != nil { + return found + } + } + } + return nil +} + +func (m *Menu) RemoveMenuItem(target *MenuItem) { + for i, item := range m.items { + if item == target { + // Remove the item from the slice + m.items = append(m.items[:i], m.items[i+1:]...) + break + } + if item.submenu != nil { + item.submenu.RemoveMenuItem(target) + } + } +} + +// ItemAt returns the menu item at the given index, or nil if the index is out of bounds. +func (m *Menu) ItemAt(index int) *MenuItem { + if index < 0 || index >= len(m.items) { + return nil + } + return m.items[index] +} + // Clone recursively clones the menu and all its submenus. -func (m *Menu) clone() *Menu { +func (m *Menu) Clone() *Menu { result := &Menu{ label: m.label, } for _, item := range m.items { - result.items = append(result.items, item.clone()) + result.items = append(result.items, item.Clone()) } return result } @@ -113,3 +151,17 @@ func (m *Menu) Append(in *Menu) { func (a *App) NewMenu() *Menu { return &Menu{} } + +func NewMenuFromItems(item *MenuItem, items ...*MenuItem) *Menu { + result := &Menu{ + items: []*MenuItem{item}, + } + result.items = append(result.items, items...) + return result +} + +func NewSubmenu(s string, items *Menu) *MenuItem { + result := NewSubMenuItem(s) + result.submenu = items + return result +} diff --git a/v3/pkg/application/menu_darwin.go b/v3/pkg/application/menu_darwin.go index 1c9c98e19..0fb3069ae 100644 --- a/v3/pkg/application/menu_darwin.go +++ b/v3/pkg/application/menu_darwin.go @@ -109,7 +109,7 @@ func (m *macosMenu) processMenu(parent unsafe.Pointer, menu *Menu) { } } -func defaultApplicationMenu() *Menu { +func DefaultApplicationMenu() *Menu { menu := NewMenu() menu.AddRole(AppMenu) menu.AddRole(FileMenu) diff --git a/v3/pkg/application/menu_linux.go b/v3/pkg/application/menu_linux.go index 045614cdc..b6235c2da 100644 --- a/v3/pkg/application/menu_linux.go +++ b/v3/pkg/application/menu_linux.go @@ -106,7 +106,7 @@ func (l *linuxMenu) createMenu(name string, items []*MenuItem) *Menu { return menu } -func defaultApplicationMenu() *Menu { +func DefaultApplicationMenu() *Menu { menu := NewMenu() menu.AddRole(AppMenu) menu.AddRole(FileMenu) diff --git a/v3/pkg/application/menu_test.go b/v3/pkg/application/menu_test.go new file mode 100644 index 000000000..88aeb5724 --- /dev/null +++ b/v3/pkg/application/menu_test.go @@ -0,0 +1,140 @@ +package application_test + +import ( + "testing" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +func TestMenu_FindByLabel(t *testing.T) { + tests := []struct { + name string + menu *application.Menu + label string + shouldError bool + }{ + { + name: "Find top-level item", + menu: application.NewMenuFromItems( + application.NewMenuItem("Target"), + ), + label: "Target", + shouldError: false, + }, + { + name: "Find item in submenu", + menu: application.NewMenuFromItems( + application.NewMenuItem("Item 1"), + application.NewSubmenu("Submenu", application.NewMenuFromItems( + application.NewMenuItem("Subitem 1"), + application.NewMenuItem("Target"), + )), + ), + label: "Target", + shouldError: false, + }, + { + name: "Not find item", + menu: application.NewMenuFromItems( + application.NewMenuItem("Item 1"), + application.NewSubmenu("Submenu", application.NewMenuFromItems( + application.NewMenuItem("Subitem 1"), + application.NewMenuItem("Target"), + )), + ), + label: "Random", + shouldError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + found := test.menu.FindByLabel(test.label) + if test.shouldError && found != nil { + t.Errorf("Expected error, but found %v", found) + } + if !test.shouldError && found == nil { + t.Errorf("Expected item, but found none") + } + }) + } +} + +func TestMenu_ItemAt(t *testing.T) { + tests := []struct { + name string + menu *application.Menu + index int + shouldError bool + }{ + { + name: "Valid index", + menu: application.NewMenuFromItems( + application.NewMenuItem("Item 1"), + application.NewMenuItem("Item 2"), + application.NewMenuItem("Target"), + ), + index: 2, + shouldError: false, + }, + { + name: "Index out of bounds (negative)", + menu: application.NewMenuFromItems( + application.NewMenuItem("Item 1"), + application.NewMenuItem("Item 2"), + ), + index: -1, + shouldError: true, + }, + { + name: "Index out of bounds (too large)", + menu: application.NewMenuFromItems( + application.NewMenuItem("Item 1"), + application.NewMenuItem("Item 2"), + ), + index: 2, + shouldError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + item := test.menu.ItemAt(test.index) + if test.shouldError && item != nil { + t.Errorf("Expected error, but found %v", item) + } + if !test.shouldError && item == nil { + t.Errorf("Expected item, but found none") + } + }) + } +} + +func TestMenu_RemoveMenuItem(t *testing.T) { + itemToRemove := application.NewMenuItem("Target") + itemToKeep := application.NewMenuItem("Item 1") + + tests := []struct { + name string + menu *application.Menu + item *application.MenuItem + shouldFind bool + }{ + { + name: "Remove existing item", + menu: application.NewMenuFromItems(itemToKeep, itemToRemove), + item: itemToRemove, + shouldFind: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.menu.RemoveMenuItem(test.item) + found := test.menu.FindByLabel(test.item.Label()) + if !test.shouldFind && found != nil { + t.Errorf("Expected item to be removed, but found %v", found) + } + }) + } +} diff --git a/v3/pkg/application/menu_windows.go b/v3/pkg/application/menu_windows.go index fbb58caf7..375914569 100644 --- a/v3/pkg/application/menu_windows.go +++ b/v3/pkg/application/menu_windows.go @@ -113,7 +113,7 @@ func (w *windowsMenu) ProcessCommand(cmdMsgID int) { item.handleClick() } -func defaultApplicationMenu() *Menu { +func DefaultApplicationMenu() *Menu { menu := NewMenu() menu.AddRole(FileMenu) menu.AddRole(EditMenu) diff --git a/v3/pkg/application/menuitem.go b/v3/pkg/application/menuitem.go index 1f05874e8..c391e61e9 100644 --- a/v3/pkg/application/menuitem.go +++ b/v3/pkg/application/menuitem.go @@ -62,7 +62,7 @@ type MenuItem struct { radioGroupMembers []*MenuItem } -func newMenuItem(label string) *MenuItem { +func NewMenuItem(label string) *MenuItem { result := &MenuItem{ id: uint(atomic.AddUintptr(&menuItemID, 1)), label: label, @@ -72,7 +72,7 @@ func newMenuItem(label string) *MenuItem { return result } -func newMenuItemSeparator() *MenuItem { +func NewMenuItemSeparator() *MenuItem { result := &MenuItem{ id: uint(atomic.AddUintptr(&menuItemID, 1)), itemType: separator, @@ -80,7 +80,7 @@ func newMenuItemSeparator() *MenuItem { return result } -func newMenuItemCheckbox(label string, checked bool) *MenuItem { +func NewMenuItemCheckbox(label string, checked bool) *MenuItem { result := &MenuItem{ id: uint(atomic.AddUintptr(&menuItemID, 1)), label: label, @@ -91,7 +91,7 @@ func newMenuItemCheckbox(label string, checked bool) *MenuItem { return result } -func newMenuItemRadio(label string, checked bool) *MenuItem { +func NewMenuItemRadio(label string, checked bool) *MenuItem { result := &MenuItem{ id: uint(atomic.AddUintptr(&menuItemID, 1)), label: label, @@ -102,7 +102,7 @@ func newMenuItemRadio(label string, checked bool) *MenuItem { return result } -func newSubMenuItem(label string) *MenuItem { +func NewSubMenuItem(label string) *MenuItem { result := &MenuItem{ id: uint(atomic.AddUintptr(&menuItemID, 1)), label: label, @@ -115,7 +115,7 @@ func newSubMenuItem(label string) *MenuItem { return result } -func newRole(role Role) *MenuItem { +func NewRole(role Role) *MenuItem { switch role { case AppMenu: return newAppMenu() @@ -126,7 +126,7 @@ func newRole(role Role) *MenuItem { case ViewMenu: return newViewMenu() case ServicesMenu: - return newServicesMenu() + return NewServicesMenu() case SpeechMenu: return newSpeechMenu() case WindowMenu: @@ -189,8 +189,8 @@ func newRole(role Role) *MenuItem { return nil } -func newServicesMenu() *MenuItem { - serviceMenu := newSubMenuItem("Services") +func NewServicesMenu() *MenuItem { + serviceMenu := NewSubMenuItem("Services") serviceMenu.role = ServicesMenu return serviceMenu } @@ -237,6 +237,17 @@ func (m *MenuItem) SetAccelerator(shortcut string) *MenuItem { return m } +func (m *MenuItem) GetAccelerator() string { + if m.accelerator == nil { + return "" + } + return m.accelerator.String() +} + +func (m *MenuItem) RemoveAccelerator() { + m.accelerator = nil +} + func (m *MenuItem) SetTooltip(s string) *MenuItem { m.tooltip = s if m.impl != nil { @@ -285,6 +296,12 @@ func (m *MenuItem) SetHidden(hidden bool) *MenuItem { return m } +// GetSubmenu returns the submenu of the MenuItem. +// If the MenuItem is not a submenu, it returns nil. +func (m *MenuItem) GetSubmenu() *Menu { + return m.submenu +} + func (m *MenuItem) Checked() bool { return m.checked } @@ -333,8 +350,8 @@ func (m *MenuItem) setContextData(data *ContextMenuData) { } } -// clone returns a deep copy of the MenuItem -func (m *MenuItem) clone() *MenuItem { +// Clone returns a deep copy of the MenuItem +func (m *MenuItem) Clone() *MenuItem { result := &MenuItem{ id: m.id, label: m.label, @@ -348,7 +365,7 @@ func (m *MenuItem) clone() *MenuItem { role: m.role, } if m.submenu != nil { - result.submenu = m.submenu.clone() + result.submenu = m.submenu.Clone() } if m.accelerator != nil { result.accelerator = m.accelerator.clone() diff --git a/v3/pkg/application/menuitem_darwin.go b/v3/pkg/application/menuitem_darwin.go index 3b25b56e2..8b1217e15 100644 --- a/v3/pkg/application/menuitem_darwin.go +++ b/v3/pkg/application/menuitem_darwin.go @@ -419,13 +419,13 @@ func newSpeechMenu() *MenuItem { OnClick(func(ctx *Context) { C.stopSpeaking() }) - subMenu := newSubMenuItem("Speech") + subMenu := NewSubMenuItem("Speech") subMenu.submenu = speechMenu return subMenu } func newHideMenuItem() *MenuItem { - return newMenuItem("Hide " + globalApplication.options.Name). + return NewMenuItem("Hide " + globalApplication.options.Name). SetAccelerator("CmdOrCtrl+h"). OnClick(func(ctx *Context) { C.hideApplication() @@ -433,7 +433,7 @@ func newHideMenuItem() *MenuItem { } func newHideOthersMenuItem() *MenuItem { - return newMenuItem("Hide Others"). + return NewMenuItem("Hide Others"). SetAccelerator("CmdOrCtrl+OptionOrAlt+h"). OnClick(func(ctx *Context) { C.hideOthers() @@ -441,14 +441,14 @@ func newHideOthersMenuItem() *MenuItem { } func newUnhideMenuItem() *MenuItem { - return newMenuItem("Show All"). + return NewMenuItem("Show All"). OnClick(func(ctx *Context) { C.showAll() }) } func newUndoMenuItem() *MenuItem { - return newMenuItem("Undo"). + return NewMenuItem("Undo"). SetAccelerator("CmdOrCtrl+z"). OnClick(func(ctx *Context) { C.undo() @@ -457,7 +457,7 @@ func newUndoMenuItem() *MenuItem { // newRedoMenuItem creates a new menu item for redoing the last action func newRedoMenuItem() *MenuItem { - return newMenuItem("Redo"). + return NewMenuItem("Redo"). SetAccelerator("CmdOrCtrl+Shift+z"). OnClick(func(ctx *Context) { C.redo() @@ -465,7 +465,7 @@ func newRedoMenuItem() *MenuItem { } func newCutMenuItem() *MenuItem { - return newMenuItem("Cut"). + return NewMenuItem("Cut"). SetAccelerator("CmdOrCtrl+x"). OnClick(func(ctx *Context) { C.cut() @@ -473,7 +473,7 @@ func newCutMenuItem() *MenuItem { } func newCopyMenuItem() *MenuItem { - return newMenuItem("Copy"). + return NewMenuItem("Copy"). SetAccelerator("CmdOrCtrl+c"). OnClick(func(ctx *Context) { C.copy() @@ -481,7 +481,7 @@ func newCopyMenuItem() *MenuItem { } func newPasteMenuItem() *MenuItem { - return newMenuItem("Paste"). + return NewMenuItem("Paste"). SetAccelerator("CmdOrCtrl+v"). OnClick(func(ctx *Context) { C.paste() @@ -489,7 +489,7 @@ func newPasteMenuItem() *MenuItem { } func newPasteAndMatchStyleMenuItem() *MenuItem { - return newMenuItem("Paste and Match Style"). + return NewMenuItem("Paste and Match Style"). SetAccelerator("CmdOrCtrl+OptionOrAlt+Shift+v"). OnClick(func(ctx *Context) { C.pasteAndMatchStyle() @@ -497,7 +497,7 @@ func newPasteAndMatchStyleMenuItem() *MenuItem { } func newDeleteMenuItem() *MenuItem { - return newMenuItem("Delete"). + return NewMenuItem("Delete"). SetAccelerator("backspace"). OnClick(func(ctx *Context) { C.delete() @@ -505,7 +505,7 @@ func newDeleteMenuItem() *MenuItem { } func newQuitMenuItem() *MenuItem { - return newMenuItem("Quit " + globalApplication.options.Name). + return NewMenuItem("Quit " + globalApplication.options.Name). SetAccelerator("CmdOrCtrl+q"). OnClick(func(ctx *Context) { globalApplication.Quit() @@ -513,7 +513,7 @@ func newQuitMenuItem() *MenuItem { } func newSelectAllMenuItem() *MenuItem { - return newMenuItem("Select All"). + return NewMenuItem("Select All"). SetAccelerator("CmdOrCtrl+a"). OnClick(func(ctx *Context) { C.selectAll() @@ -521,14 +521,14 @@ func newSelectAllMenuItem() *MenuItem { } func newAboutMenuItem() *MenuItem { - return newMenuItem("About " + globalApplication.options.Name). + return NewMenuItem("About " + globalApplication.options.Name). OnClick(func(ctx *Context) { globalApplication.ShowAboutDialog() }) } func newCloseMenuItem() *MenuItem { - return newMenuItem("Close"). + return NewMenuItem("Close"). SetAccelerator("CmdOrCtrl+w"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -539,7 +539,7 @@ func newCloseMenuItem() *MenuItem { } func newReloadMenuItem() *MenuItem { - return newMenuItem("Reload"). + return NewMenuItem("Reload"). SetAccelerator("CmdOrCtrl+r"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -550,7 +550,7 @@ func newReloadMenuItem() *MenuItem { } func newForceReloadMenuItem() *MenuItem { - return newMenuItem("Force Reload"). + return NewMenuItem("Force Reload"). SetAccelerator("CmdOrCtrl+Shift+r"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -561,7 +561,7 @@ func newForceReloadMenuItem() *MenuItem { } func newToggleFullscreenMenuItem() *MenuItem { - result := newMenuItem("Toggle Full Screen"). + result := NewMenuItem("Toggle Full Screen"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() if currentWindow != nil { @@ -578,7 +578,7 @@ func newToggleFullscreenMenuItem() *MenuItem { func newZoomResetMenuItem() *MenuItem { // reset zoom menu item - return newMenuItem("Actual Size"). + return NewMenuItem("Actual Size"). SetAccelerator("CmdOrCtrl+0"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -589,7 +589,7 @@ func newZoomResetMenuItem() *MenuItem { } func newZoomInMenuItem() *MenuItem { - return newMenuItem("Zoom In"). + return NewMenuItem("Zoom In"). SetAccelerator("CmdOrCtrl+plus"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -600,7 +600,7 @@ func newZoomInMenuItem() *MenuItem { } func newZoomOutMenuItem() *MenuItem { - return newMenuItem("Zoom Out"). + return NewMenuItem("Zoom Out"). SetAccelerator("CmdOrCtrl+-"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -611,7 +611,7 @@ func newZoomOutMenuItem() *MenuItem { } func newMinimizeMenuItem() *MenuItem { - return newMenuItem("Minimize"). + return NewMenuItem("Minimize"). SetAccelerator("CmdOrCtrl+M"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -622,7 +622,7 @@ func newMinimizeMenuItem() *MenuItem { } func newZoomMenuItem() *MenuItem { - return newMenuItem("Zoom"). + return NewMenuItem("Zoom"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() if currentWindow != nil { @@ -632,7 +632,7 @@ func newZoomMenuItem() *MenuItem { } func newFullScreenMenuItem() *MenuItem { - return newMenuItem("Fullscreen"). + return NewMenuItem("Fullscreen"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() if currentWindow != nil { diff --git a/v3/pkg/application/menuitem_dev.go b/v3/pkg/application/menuitem_dev.go index 7375b117d..24f6f06bd 100644 --- a/v3/pkg/application/menuitem_dev.go +++ b/v3/pkg/application/menuitem_dev.go @@ -3,7 +3,7 @@ package application func newOpenDevToolsMenuItem() *MenuItem { - return newMenuItem("Open Developer Tools"). + return NewMenuItem("Open Developer Tools"). SetAccelerator("Alt+Command+I"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() diff --git a/v3/pkg/application/menuitem_linux.go b/v3/pkg/application/menuitem_linux.go index abdd8fe05..4c1447c03 100644 --- a/v3/pkg/application/menuitem_linux.go +++ b/v3/pkg/application/menuitem_linux.go @@ -136,13 +136,13 @@ func newSpeechMenu() *MenuItem { OnClick(func(ctx *Context) { // C.stopSpeaking() }) - subMenu := newSubMenuItem("Speech") + subMenu := NewSubMenuItem("Speech") subMenu.submenu = speechMenu return subMenu } func newHideMenuItem() *MenuItem { - return newMenuItem("Hide " + globalApplication.options.Name). + return NewMenuItem("Hide " + globalApplication.options.Name). SetAccelerator("CmdOrCtrl+h"). OnClick(func(ctx *Context) { @@ -151,7 +151,7 @@ func newHideMenuItem() *MenuItem { } func newHideOthersMenuItem() *MenuItem { - return newMenuItem("Hide Others"). + return NewMenuItem("Hide Others"). SetAccelerator("CmdOrCtrl+OptionOrAlt+h"). OnClick(func(ctx *Context) { // C.hideOthers() @@ -159,14 +159,14 @@ func newHideOthersMenuItem() *MenuItem { } func newUnhideMenuItem() *MenuItem { - return newMenuItem("Show All"). + return NewMenuItem("Show All"). OnClick(func(ctx *Context) { // C.showAll() }) } func newUndoMenuItem() *MenuItem { - return newMenuItem("Undo"). + return NewMenuItem("Undo"). SetAccelerator("CmdOrCtrl+z"). OnClick(func(ctx *Context) { // C.undo() @@ -175,7 +175,7 @@ func newUndoMenuItem() *MenuItem { // newRedoMenuItem creates a new menu item for redoing the last action func newRedoMenuItem() *MenuItem { - return newMenuItem("Redo"). + return NewMenuItem("Redo"). SetAccelerator("CmdOrCtrl+Shift+z"). OnClick(func(ctx *Context) { // C.redo() @@ -183,7 +183,7 @@ func newRedoMenuItem() *MenuItem { } func newCutMenuItem() *MenuItem { - return newMenuItem("Cut"). + return NewMenuItem("Cut"). SetAccelerator("CmdOrCtrl+x"). OnClick(func(ctx *Context) { // C.cut() @@ -191,7 +191,7 @@ func newCutMenuItem() *MenuItem { } func newCopyMenuItem() *MenuItem { - return newMenuItem("Copy"). + return NewMenuItem("Copy"). SetAccelerator("CmdOrCtrl+c"). OnClick(func(ctx *Context) { // C.copy() @@ -199,7 +199,7 @@ func newCopyMenuItem() *MenuItem { } func newPasteMenuItem() *MenuItem { - return newMenuItem("Paste"). + return NewMenuItem("Paste"). SetAccelerator("CmdOrCtrl+v"). OnClick(func(ctx *Context) { // C.paste() @@ -207,7 +207,7 @@ func newPasteMenuItem() *MenuItem { } func newPasteAndMatchStyleMenuItem() *MenuItem { - return newMenuItem("Paste and Match Style"). + return NewMenuItem("Paste and Match Style"). SetAccelerator("CmdOrCtrl+OptionOrAlt+Shift+v"). OnClick(func(ctx *Context) { // C.pasteAndMatchStyle() @@ -215,7 +215,7 @@ func newPasteAndMatchStyleMenuItem() *MenuItem { } func newDeleteMenuItem() *MenuItem { - return newMenuItem("Delete"). + return NewMenuItem("Delete"). SetAccelerator("backspace"). OnClick(func(ctx *Context) { // C.delete() @@ -223,7 +223,7 @@ func newDeleteMenuItem() *MenuItem { } func newQuitMenuItem() *MenuItem { - return newMenuItem("Quit " + globalApplication.options.Name). + return NewMenuItem("Quit " + globalApplication.options.Name). SetAccelerator("CmdOrCtrl+q"). OnClick(func(ctx *Context) { globalApplication.Quit() @@ -231,7 +231,7 @@ func newQuitMenuItem() *MenuItem { } func newSelectAllMenuItem() *MenuItem { - return newMenuItem("Select All"). + return NewMenuItem("Select All"). SetAccelerator("CmdOrCtrl+a"). OnClick(func(ctx *Context) { // C.selectAll() @@ -239,14 +239,14 @@ func newSelectAllMenuItem() *MenuItem { } func newAboutMenuItem() *MenuItem { - return newMenuItem("About " + globalApplication.options.Name). + return NewMenuItem("About " + globalApplication.options.Name). OnClick(func(ctx *Context) { globalApplication.ShowAboutDialog() }) } func newCloseMenuItem() *MenuItem { - return newMenuItem("Close"). + return NewMenuItem("Close"). SetAccelerator("CmdOrCtrl+w"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -257,7 +257,7 @@ func newCloseMenuItem() *MenuItem { } func newReloadMenuItem() *MenuItem { - return newMenuItem("Reload"). + return NewMenuItem("Reload"). SetAccelerator("CmdOrCtrl+r"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -268,7 +268,7 @@ func newReloadMenuItem() *MenuItem { } func newForceReloadMenuItem() *MenuItem { - return newMenuItem("Force Reload"). + return NewMenuItem("Force Reload"). SetAccelerator("CmdOrCtrl+Shift+r"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -279,7 +279,7 @@ func newForceReloadMenuItem() *MenuItem { } func newToggleFullscreenMenuItem() *MenuItem { - result := newMenuItem("Toggle Full Screen"). + result := NewMenuItem("Toggle Full Screen"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() if currentWindow != nil { @@ -296,7 +296,7 @@ func newToggleFullscreenMenuItem() *MenuItem { func newZoomResetMenuItem() *MenuItem { // reset zoom menu item - return newMenuItem("Actual Size"). + return NewMenuItem("Actual Size"). SetAccelerator("CmdOrCtrl+0"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -307,7 +307,7 @@ func newZoomResetMenuItem() *MenuItem { } func newZoomInMenuItem() *MenuItem { - return newMenuItem("Zoom In"). + return NewMenuItem("Zoom In"). SetAccelerator("CmdOrCtrl+plus"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -318,7 +318,7 @@ func newZoomInMenuItem() *MenuItem { } func newZoomOutMenuItem() *MenuItem { - return newMenuItem("Zoom Out"). + return NewMenuItem("Zoom Out"). SetAccelerator("CmdOrCtrl+-"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -329,7 +329,7 @@ func newZoomOutMenuItem() *MenuItem { } func newMinimizeMenuItem() *MenuItem { - return newMenuItem("Minimize"). + return NewMenuItem("Minimize"). SetAccelerator("CmdOrCtrl+M"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -340,7 +340,7 @@ func newMinimizeMenuItem() *MenuItem { } func newZoomMenuItem() *MenuItem { - return newMenuItem("Zoom"). + return NewMenuItem("Zoom"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() if currentWindow != nil { @@ -350,7 +350,7 @@ func newZoomMenuItem() *MenuItem { } func newFullScreenMenuItem() *MenuItem { - return newMenuItem("Fullscreen"). + return NewMenuItem("Fullscreen"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() if currentWindow != nil { diff --git a/v3/pkg/application/menuitem_test.go b/v3/pkg/application/menuitem_test.go new file mode 100644 index 000000000..bb325fcfa --- /dev/null +++ b/v3/pkg/application/menuitem_test.go @@ -0,0 +1,61 @@ +package application_test + +import ( + "testing" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +func TestMenuItem_GetAccelerator(t *testing.T) { + tests := []struct { + name string + menuItem *application.MenuItem + expectedAcc string + }{ + { + name: "Get existing accelerator", + menuItem: application.NewMenuItem("Item 1").SetAccelerator("Ctrl+A"), + expectedAcc: "ctrl+a", + }, + { + name: "Get non-existing accelerator", + menuItem: application.NewMenuItem("Item 2"), + expectedAcc: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + acc := test.menuItem.GetAccelerator() + if acc != test.expectedAcc { + t.Errorf("Expected accelerator to be %v, but got %v", test.expectedAcc, acc) + } + }) + } +} + +func TestMenuItem_RemoveAccelerator(t *testing.T) { + tests := []struct { + name string + menuItem *application.MenuItem + }{ + { + name: "Remove existing accelerator", + menuItem: application.NewMenuItem("Item 1").SetAccelerator("Ctrl+A"), + }, + { + name: "Remove non-existing accelerator", + menuItem: application.NewMenuItem("Item 2"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.menuItem.RemoveAccelerator() + acc := test.menuItem.GetAccelerator() + if acc != "" { + t.Errorf("Expected accelerator to be removed, but got %v", acc) + } + }) + } +} diff --git a/v3/pkg/application/menuitem_windows.go b/v3/pkg/application/menuitem_windows.go index 2a46f8da6..8342bdb97 100644 --- a/v3/pkg/application/menuitem_windows.go +++ b/v3/pkg/application/menuitem_windows.go @@ -191,7 +191,7 @@ func newDeleteMenuItem() *MenuItem { } func newQuitMenuItem() *MenuItem { - return newMenuItem("Quit"). + return NewMenuItem("Quit"). OnClick(func(ctx *Context) { globalApplication.Quit() }) @@ -203,14 +203,14 @@ func newSelectAllMenuItem() *MenuItem { } func newAboutMenuItem() *MenuItem { - return newMenuItem("About " + globalApplication.options.Name). + return NewMenuItem("About " + globalApplication.options.Name). OnClick(func(ctx *Context) { globalApplication.ShowAboutDialog() }) } func newCloseMenuItem() *MenuItem { - return newMenuItem("Close"). + return NewMenuItem("Close"). SetAccelerator("CmdOrCtrl+w"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -220,7 +220,7 @@ func newCloseMenuItem() *MenuItem { }) } func newReloadMenuItem() *MenuItem { - return newMenuItem("Reload"). + return NewMenuItem("Reload"). SetAccelerator("CmdOrCtrl+r"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -231,7 +231,7 @@ func newReloadMenuItem() *MenuItem { } func newForceReloadMenuItem() *MenuItem { - return newMenuItem("Force Reload"). + return NewMenuItem("Force Reload"). SetAccelerator("CmdOrCtrl+Shift+r"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -242,7 +242,7 @@ func newForceReloadMenuItem() *MenuItem { } func newToggleFullscreenMenuItem() *MenuItem { - result := newMenuItem("Toggle Full Screen"). + result := NewMenuItem("Toggle Full Screen"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() if currentWindow != nil { @@ -259,7 +259,7 @@ func newToggleFullscreenMenuItem() *MenuItem { func newZoomResetMenuItem() *MenuItem { // reset zoom menu item - return newMenuItem("Actual Size"). + return NewMenuItem("Actual Size"). SetAccelerator("CmdOrCtrl+0"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -270,7 +270,7 @@ func newZoomResetMenuItem() *MenuItem { } func newZoomInMenuItem() *MenuItem { - return newMenuItem("Zoom In"). + return NewMenuItem("Zoom In"). SetAccelerator("CmdOrCtrl+plus"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -281,7 +281,7 @@ func newZoomInMenuItem() *MenuItem { } func newZoomOutMenuItem() *MenuItem { - return newMenuItem("Zoom Out"). + return NewMenuItem("Zoom Out"). SetAccelerator("CmdOrCtrl+-"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -292,7 +292,7 @@ func newZoomOutMenuItem() *MenuItem { } func newFullScreenMenuItem() *MenuItem { - return newMenuItem("Fullscreen"). + return NewMenuItem("Fullscreen"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() if currentWindow != nil { @@ -302,7 +302,7 @@ func newFullScreenMenuItem() *MenuItem { } func newMinimizeMenuItem() *MenuItem { - return newMenuItem("Minimize"). + return NewMenuItem("Minimize"). SetAccelerator("CmdOrCtrl+M"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() @@ -313,7 +313,7 @@ func newMinimizeMenuItem() *MenuItem { } func newZoomMenuItem() *MenuItem { - return newMenuItem("Zoom"). + return NewMenuItem("Zoom"). OnClick(func(ctx *Context) { currentWindow := globalApplication.CurrentWindow() if currentWindow != nil { diff --git a/v3/pkg/application/roles.go b/v3/pkg/application/roles.go index bce6b5c2c..b5ab32923 100644 --- a/v3/pkg/application/roles.go +++ b/v3/pkg/application/roles.go @@ -67,7 +67,7 @@ func newFileMenu() *MenuItem { } else { fileMenu.AddRole(Quit) } - subMenu := newSubMenuItem("File") + subMenu := NewSubMenuItem("File") subMenu.submenu = fileMenu return subMenu } @@ -83,7 +83,7 @@ func newViewMenu() *MenuItem { viewMenu.AddRole(ZoomOut) viewMenu.AddSeparator() viewMenu.AddRole(ToggleFullscreen) - subMenu := newSubMenuItem("View") + subMenu := NewSubMenuItem("View") subMenu.submenu = viewMenu return subMenu } @@ -102,7 +102,7 @@ func newAppMenu() *MenuItem { appMenu.AddRole(UnHide) appMenu.AddSeparator() appMenu.AddRole(Quit) - subMenu := newSubMenuItem(globalApplication.options.Name) + subMenu := NewSubMenuItem(globalApplication.options.Name) subMenu.submenu = appMenu return subMenu } @@ -127,7 +127,7 @@ func newEditMenu() *MenuItem { editMenu.AddSeparator() editMenu.AddRole(SelectAll) } - subMenu := newSubMenuItem("Edit") + subMenu := NewSubMenuItem("Edit") subMenu.submenu = editMenu return subMenu } @@ -142,7 +142,7 @@ func newWindowMenu() *MenuItem { } else { menu.AddRole(Close) } - subMenu := newSubMenuItem("Window") + subMenu := NewSubMenuItem("Window") subMenu.submenu = menu return subMenu } @@ -152,7 +152,7 @@ func newHelpMenu() *MenuItem { menu.Add("Learn More").OnClick(func(ctx *Context) { globalApplication.CurrentWindow().SetURL("https://wails.io") }) - subMenu := newSubMenuItem("Help") + subMenu := NewSubMenuItem("Help") subMenu.submenu = menu return subMenu } diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index 49586cabe..cbf8edee6 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -207,7 +207,7 @@ func (w *linuxWebviewWindow) run() { var menu = w.menu if menu == nil && globalApplication.ApplicationMenu != nil { - menu = globalApplication.ApplicationMenu.clone() + menu = globalApplication.ApplicationMenu.Clone() } if menu != nil { InvokeSync(func() {