diff --git a/docs/src/content/docs/changelog.mdx b/docs/src/content/docs/changelog.mdx index 5802f7696..f63babaa4 100644 --- a/docs/src/content/docs/changelog.mdx +++ b/docs/src/content/docs/changelog.mdx @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New Menu guide by [@leaanthony](https://github.com/leaanthony) - Add doc comments for Service API by [@fbbdev](https://github.com/fbbdev) in [#4024](https://github.com/wailsapp/wails/pull/4024) - Add function `application.NewServiceWithOptions` to initialise services with additional configuration by [@leaanthony](https://github.com/leaanthony) in [#4024](https://github.com/wailsapp/wails/pull/4024) +- Improved menu control by [@FalcoG](https://github.com/FalcoG) and [@leaanthony](https://github.com/leaanthony) in [#4031](https://github.com/wailsapp/wails/pull/4031) - More documentation by [@leaanthony](https://github.com/leaanthony) - Support cancellation of events in standard event listeners by [@leaanthony](https://github.com/leaanthony) - Systray `Hide`, `Show` and `Destroy` support by [@leaanthony](https://github.com/leaanthony) diff --git a/docs/src/content/docs/guides/menus.mdx b/docs/src/content/docs/guides/menus.mdx index d3bc165b6..58a9cd313 100644 --- a/docs/src/content/docs/guides/menus.mdx +++ b/docs/src/content/docs/guides/menus.mdx @@ -56,6 +56,65 @@ submenu.Add("Open") submenu.Add("Save") ``` +#### Combining menus +A menu can be added into another menu by appending or prepending it. +```go +menu := application.NewMenu() +menu.Add("First Menu") + +secondaryMenu := application.NewMenu() +secondaryMenu.Add("Second Menu") + +// insert 'secondaryMenu' after 'menu' +menu.Append(secondaryMenu) + +// insert 'secondaryMenu' before 'menu' +menu.Prepend(secondaryMenu) + +// update the menu +menu.Update() +``` + +:::note +By default, `prepend` and `append` will share state with the original menu. If you want to create a new menu with its own state, +you can call `.Clone()` on the menu. + +E.g: `menu.Append(secondaryMenu.Clone())` +::: + +#### Clearing a menu +In some cases it'll be better to construct a whole new menu if you are working with a variable amount of menu items. + +This will clear all items on an existing menu and allows you to add items again. + +```go +menu := application.NewMenu() +menu.Add("Waiting for update...") + +// after certain logic, the menu has to be updated +menu.Clear() +menu.Add("Update complete!") +menu.Update() +``` + +:::note +Clearing a menu simply clears the menu items at the top level. Whilst Submenus won't be visible, they will still occupy memory +so be sure to manage your menus carefully. +::: + +#### Destroying a menu + +If you want to clear and release a menu, use the `Destroy()` method: + +```go +menu := application.NewMenu() +menu.Add("Waiting for update...") + +// after certain logic, the menu has to be destroyed +menu.Destroy() +``` + + ### Menu Item Properties Menu items have several properties that can be configured: diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index 86d930f03..33006a0c2 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -587,6 +587,10 @@ func menuItemNew(label string, bitmap []byte) pointer { return menuItemAddProperties(C.gtk_menu_item_new(), label, bitmap) } +func menuItemDestroy(widget pointer) { + C.gtk_widget_destroy((*C.GtkWidget)(widget)) +} + func menuItemAddProperties(menuItem *C.GtkWidget, label string, bitmap []byte) pointer { /* // FIXME: Support accelerator configuration diff --git a/v3/pkg/application/menu.go b/v3/pkg/application/menu.go index 5a8673ea7..935000e20 100644 --- a/v3/pkg/application/menu.go +++ b/v3/pkg/application/menu.go @@ -69,6 +69,21 @@ func (m *Menu) Update() { m.impl.update() } +// Clear all menu items +func (m *Menu) Clear() { + for _, item := range m.items { + removeMenuItemByID(item.id) + } + m.items = nil +} + +func (m *Menu) Destroy() { + for _, item := range m.items { + item.Destroy() + } + m.items = nil +} + func (m *Menu) AddSubmenu(s string) *Menu { result := NewSubMenuItem(s) m.items = append(m.items, result) @@ -186,10 +201,19 @@ func (m *Menu) Clone() *Menu { return result } +// Append menu to an existing menu func (m *Menu) Append(in *Menu) { + if in == nil { + return + } m.items = append(m.items, in.items...) } +// Prepend menu before an existing menu +func (m *Menu) Prepend(in *Menu) { + m.items = append(in.items, m.items...) +} + func (a *App) NewMenu() *Menu { return &Menu{} } diff --git a/v3/pkg/application/menuitem.go b/v3/pkg/application/menuitem.go index 7bce23ee7..ab5371e24 100644 --- a/v3/pkg/application/menuitem.go +++ b/v3/pkg/application/menuitem.go @@ -33,6 +33,12 @@ func getMenuItemByID(id uint) *MenuItem { return menuItemMap[id] } +func removeMenuItemByID(id uint) { + menuItemMapLock.Lock() + defer menuItemMapLock.Unlock() + delete(menuItemMap, id) +} + type menuItemImpl interface { setTooltip(s string) setLabel(s string) @@ -41,6 +47,7 @@ type menuItemImpl interface { setAccelerator(accelerator *accelerator) setHidden(hidden bool) setBitmap(bitmap []byte) + destroy() } type MenuItem struct { @@ -425,3 +432,29 @@ func (m *MenuItem) Clone() *MenuItem { } return result } + +func (m *MenuItem) Destroy() { + + removeMenuItemByID(m.id) + + // Clean up resources + if m.impl != nil { + m.impl.destroy() + } + if m.submenu != nil { + m.submenu.Destroy() + m.submenu = nil + } + + if m.contextMenuData != nil { + m.contextMenuData = nil + } + + if m.accelerator != nil { + m.accelerator = nil + } + + m.callback = nil + m.radioGroupMembers = nil + +} diff --git a/v3/pkg/application/menuitem_darwin.go b/v3/pkg/application/menuitem_darwin.go index 1f65ef331..e3d3401da 100644 --- a/v3/pkg/application/menuitem_darwin.go +++ b/v3/pkg/application/menuitem_darwin.go @@ -295,6 +295,11 @@ void setMenuItemBitmap(void* nsMenuItem, unsigned char *bitmap, int length) { NSImage *image = [[NSImage alloc] initWithData:[NSData dataWithBytes:bitmap length:length]]; [menuItem setImage:image]; } + +void destroyMenuItem(void* nsMenuItem) { + MenuItem *menuItem = (MenuItem *)nsMenuItem; + [menuItem release]; +} */ import "C" import ( @@ -344,6 +349,10 @@ func (m macosMenuItem) setAccelerator(accelerator *accelerator) { C.setMenuItemKeyEquivalent(m.nsMenuItem, key, modifier) } +func (m macosMenuItem) destroy() { + C.destroyMenuItem(m.nsMenuItem) +} + func newMenuItemImpl(item *MenuItem) *macosMenuItem { result := &macosMenuItem{ menuItem: item, diff --git a/v3/pkg/application/menuitem_linux.go b/v3/pkg/application/menuitem_linux.go index ca22d7452..34dac1f02 100644 --- a/v3/pkg/application/menuitem_linux.go +++ b/v3/pkg/application/menuitem_linux.go @@ -21,6 +21,14 @@ func (l linuxMenuItem) setTooltip(tooltip string) { }) } +func (l linuxMenuItem) destroy() { + InvokeSync(func() { + l.blockSignal() + defer l.unBlockSignal() + menuItemDestroy(l.native) + }) +} + func (l linuxMenuItem) blockSignal() { if l.handlerId != 0 { menuItemSignalBlock(l.native, l.handlerId, true) diff --git a/v3/pkg/application/menuitem_windows.go b/v3/pkg/application/menuitem_windows.go index b880d54d3..b13ef6f5a 100644 --- a/v3/pkg/application/menuitem_windows.go +++ b/v3/pkg/application/menuitem_windows.go @@ -96,6 +96,10 @@ func (m *windowsMenuItem) setChecked(checked bool) { m.update() } +func (m *windowsMenuItem) destroy() { + w32.RemoveMenu(m.hMenu, m.id, w32.MF_BYCOMMAND) +} + func (m *windowsMenuItem) setAccelerator(accelerator *accelerator) { //// Set the keyboard shortcut of the menu item //var modifier C.int diff --git a/v3/pkg/application/systemtray_linux.go b/v3/pkg/application/systemtray_linux.go index fba5cc65a..0bf25b6a0 100644 --- a/v3/pkg/application/systemtray_linux.go +++ b/v3/pkg/application/systemtray_linux.go @@ -87,6 +87,8 @@ func (s *systrayMenuItem) setDisabled(disabled bool) { } } +func (s *systrayMenuItem) destroy() {} + func (s *systrayMenuItem) setChecked(checked bool) { v := dbus.MakeVariant(0) if checked {