diff --git a/v2/internal/messagedispatcher/messagedispatcher.go b/v2/internal/messagedispatcher/messagedispatcher.go index 2a2a610cd..e10d05504 100644 --- a/v2/internal/messagedispatcher/messagedispatcher.go +++ b/v2/internal/messagedispatcher/messagedispatcher.go @@ -71,7 +71,7 @@ func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher, return nil, err } - menuChannel, err := servicebus.Subscribe("menu:") + menuChannel, err := servicebus.Subscribe("menufrontend:") if err != nil { return nil, err } @@ -422,7 +422,8 @@ func (d *Dispatcher) processMenuMessage(result *servicebus.Message) { updatedMenu, ok := result.Data().(*menu.Menu) if !ok { - d.logger.Error("Invalid data for 'dialog:select:open' : %#v", result.Data()) + d.logger.Error("Invalid data for 'menufrontend:update' : %#v", + result.Data()) return } @@ -433,6 +434,6 @@ func (d *Dispatcher) processMenuMessage(result *servicebus.Message) { } default: - d.logger.Error("Unknown dialog command: %s", command) + d.logger.Error("Unknown menufrontend command: %s", command) } } diff --git a/v2/internal/runtime/menu.go b/v2/internal/runtime/menu.go index f5b53da89..284e1e511 100644 --- a/v2/internal/runtime/menu.go +++ b/v2/internal/runtime/menu.go @@ -10,6 +10,8 @@ import ( type Menu interface { On(menuID string, callback func(*menu.MenuItem)) Update() + GetByID(menuID string) *menu.MenuItem + RemoveByID(id string) bool } type menuRuntime struct { @@ -36,3 +38,11 @@ func (m *menuRuntime) On(menuID string, callback func(*menu.MenuItem)) { func (m *menuRuntime) Update() { m.bus.Publish("menu:update", m.menu) } + +func (m *menuRuntime) GetByID(menuID string) *menu.MenuItem { + return m.menu.GetByID(menuID) +} + +func (m *menuRuntime) RemoveByID(id string) bool { + return m.menu.RemoveByID(id) +} diff --git a/v2/internal/subsystem/menu.go b/v2/internal/subsystem/menu.go index bba5d4009..98f843aea 100644 --- a/v2/internal/subsystem/menu.go +++ b/v2/internal/subsystem/menu.go @@ -37,6 +37,9 @@ type Menu struct { // The application menu applicationMenu *menu.Menu + + // Service Bus + bus *servicebus.ServiceBus } // NewMenu creates a new menu subsystem @@ -49,7 +52,7 @@ func NewMenu(applicationMenu *menu.Menu, bus *servicebus.ServiceBus, logger *log } // Subscribe to menu messages - menuChannel, err := bus.Subscribe("menu") + menuChannel, err := bus.Subscribe("menu:") if err != nil { return nil, err } @@ -61,6 +64,7 @@ func NewMenu(applicationMenu *menu.Menu, bus *servicebus.ServiceBus, logger *log listeners: make(map[string][]func(*menu.MenuItem)), menuItems: make(map[string]*menu.MenuItem), applicationMenu: applicationMenu, + bus: bus, } // Build up list of item/id pairs @@ -113,13 +117,16 @@ func (m *Menu) Start() error { case "on": listenerDetails := menuMessage.Data().(*message.MenuOnMessage) id := listenerDetails.MenuID - // Check we have a menu with that id - if m.menuItems[id] == nil { - m.logger.Error("cannot register listener for unknown menu id '%s'", id) - continue - } - // We do! Append the callback m.listeners[id] = append(m.listeners[id], listenerDetails.Callback) + + // Make sure we catch any menu updates + case "update": + updatedMenu := menuMessage.Data().(*menu.Menu) + m.processMenu(updatedMenu) + + // Notify frontend of menu change + m.bus.Publish("menufrontend:update", updatedMenu) + default: m.logger.Error("unknown menu message: %+v", menuMessage) } @@ -133,8 +140,12 @@ func (m *Menu) Start() error { return nil } -func (m *Menu) processMenu(menu *menu.Menu) { - for _, item := range menu.Items { +func (m *Menu) processMenu(applicationMenu *menu.Menu) { + // Initialise the variables + m.menuItems = make(map[string]*menu.MenuItem) + m.applicationMenu = applicationMenu + + for _, item := range applicationMenu.Items { m.processMenuItem(item) } } diff --git a/v2/pkg/menu/menu.go b/v2/pkg/menu/menu.go index 4c3ae8f5a..14fce80f9 100644 --- a/v2/pkg/menu/menu.go +++ b/v2/pkg/menu/menu.go @@ -22,3 +22,30 @@ func NewMenuFromItems(first *MenuItem, rest ...*MenuItem) *Menu { return result } + +func (m *Menu) GetByID(menuID string) *MenuItem { + + // Loop over menu items + for _, item := range m.Items { + result := item.getByID(menuID) + if result != nil { + return result + } + } + return nil +} + +func (m *Menu) RemoveByID(id string) bool { + // Loop over menu items + for index, item := range m.Items { + if item.ID == id { + m.Items = append(m.Items[:index], m.Items[index+1:]...) + return true + } + result := item.removeByID(id) + if result == true { + return result + } + } + return false +} diff --git a/v2/pkg/menu/menuitem.go b/v2/pkg/menu/menuitem.go index 59e07d465..3d0426839 100644 --- a/v2/pkg/menu/menuitem.go +++ b/v2/pkg/menu/menuitem.go @@ -43,6 +43,53 @@ func (m *MenuItem) Append(item *MenuItem) bool { return true } +// Prepend will attempt to prepend the given menu item to +// this item's submenu items. If this menu item is not a +// submenu, then this method will not add the item and +// simply return false. +func (m *MenuItem) Prepend(item *MenuItem) bool { + if m.Type != SubmenuType { + return false + } + m.SubMenu = append([]*MenuItem{item}, m.SubMenu...) + return true +} + +func (m *MenuItem) getByID(id string) *MenuItem { + + // If I have the ID return me! + if m.ID == id { + return m + } + + // Check submenus + for _, submenu := range m.SubMenu { + result := submenu.getByID(id) + if result != nil { + return result + } + } + + return nil +} + +func (m *MenuItem) removeByID(id string) bool { + + for index, item := range m.SubMenu { + if item.ID == id { + m.SubMenu = append(m.SubMenu[:index], m.SubMenu[index+1:]...) + return true + } + if item.Type == SubmenuType { + result := item.removeByID(id) + if result == true { + return result + } + } + } + return false +} + // Text is a helper to create basic Text menu items func Text(label string, id string) *MenuItem { return TextWithAccelerator(label, id, nil) diff --git a/v2/test/kitchensink/go.sum b/v2/test/kitchensink/go.sum index b2f4a4543..be47204b0 100644 --- a/v2/test/kitchensink/go.sum +++ b/v2/test/kitchensink/go.sum @@ -87,6 +87,7 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/wailsapp/wails v1.8.0 h1:gnQhpwoGM8s2GD5PZrgMKU1PO3pQ9cdKKJgwtkNz2f4= github.com/wailsapp/wails v1.8.0/go.mod h1:XFZunea+USOCMMgBlz0A0JHLL3oWrRhnOl4baZlRpxo= +github.com/wailsapp/wails v1.9.1 h1:ez/TK8YpU9lvOZ9nkgzUXsWu+xOPFVO57zTy0n5w3hc= github.com/xyproto/xpm v1.2.1/go.mod h1:cMnesLsD0PBXLgjDfTDEaKr8XyTFsnP1QycSqRw7BiY= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/v2/test/kitchensink/menu.go b/v2/test/kitchensink/menu.go index c5384869c..7ade4682e 100644 --- a/v2/test/kitchensink/menu.go +++ b/v2/test/kitchensink/menu.go @@ -3,6 +3,7 @@ package main import ( "fmt" "strconv" + "sync" wails "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/menu" @@ -13,6 +14,7 @@ type Menu struct { runtime *wails.Runtime dynamicMenuCounter int + lock sync.Mutex } // WailsInit is called at application startup @@ -37,12 +39,74 @@ func (m *Menu) WailsInit(runtime *wails.Runtime) error { return nil } +func (m *Menu) incrementcounter() int { + m.dynamicMenuCounter++ + return m.dynamicMenuCounter +} + +func (m *Menu) decrementcounter() int { + m.dynamicMenuCounter-- + return m.dynamicMenuCounter +} + func (m *Menu) addMenu(mi *menu.MenuItem) { + + // Lock because this method will be called in a gorouting + m.lock.Lock() + defer m.lock.Unlock() + // Get this menu's parent parent := mi.Parent() - m.dynamicMenuCounter++ - menuText := "Dynamic Menu Item " + strconv.Itoa(m.dynamicMenuCounter) + counter := m.incrementcounter() + menuText := "Dynamic Menu Item " + strconv.Itoa(counter) parent.Append(menu.Text(menuText, menuText)) + // parent.Append(menu.TextWithAccelerator(menuText, menuText, menu.Accel("["))) + + // If this is the first dynamic menu added, let's add a remove menu item + if counter == 1 { + removeMenu := menu.TextWithAccelerator("Remove "+menuText, + "Remove Last Item", menu.CmdOrCtrlAccel("-")) + parent.Prepend(removeMenu) + m.runtime.Menu.On("Remove Last Item", m.removeMenu) + } else { + removeMenu := m.runtime.Menu.GetByID("Remove Last Item") + // Test if the remove menu hasn't already been removed in another thread + if removeMenu != nil { + removeMenu.Label = "Remove " + menuText + } + } + m.runtime.Menu.Update() +} + +func (m *Menu) removeMenu(_ *menu.MenuItem) { + + // Lock because this method will be called in a goroutine + m.lock.Lock() + defer m.lock.Unlock() + + // Get the id of the last dynamic menu + menuID := "Dynamic Menu Item " + strconv.Itoa(m.dynamicMenuCounter) + + // Remove the last menu item by ID + m.runtime.Menu.RemoveByID(menuID) + + // Update the counter + counter := m.decrementcounter() + + // If we deleted the last dynamic menu, remove the "Remove Last Item" menu + if counter == 0 { + m.runtime.Menu.RemoveByID("Remove Last Item") + } else { + // Update label + menuText := "Dynamic Menu Item " + strconv.Itoa(counter) + removeMenu := m.runtime.Menu.GetByID("Remove Last Item") + // Test if the remove menu hasn't already been removed in another thread + if removeMenu == nil { + return + } + removeMenu.Label = "Remove " + menuText + } + // parent.Append(menu.TextWithAccelerator(menuText, menuText, menu.Accel("["))) m.runtime.Menu.Update() } @@ -119,10 +183,10 @@ func createApplicationMenu() *menu.Menu { menu.TextWithAccelerator("Backtick", "Backtick", menu.Accel("`")), menu.TextWithAccelerator("Plus", "Plus", menu.Accel("+")), }), - menu.SubMenu("Dynamic Menus", []*menu.MenuItem{ - menu.TextWithAccelerator("Add Menu Item", "Add Menu Item", menu.CmdOrCtrlAccel("+")), - menu.Separator(), - }), + }), + menu.SubMenu("Dynamic Menus", []*menu.MenuItem{ + menu.TextWithAccelerator("Add Menu Item", "Add Menu Item", menu.CmdOrCtrlAccel("+")), + menu.Separator(), }), { Label: "Disabled Menu", @@ -136,12 +200,13 @@ func createApplicationMenu() *menu.Menu { Hidden: true, }, { - ID: "checkbox-menu", - Label: "Checkbox Menu", + ID: "checkbox-menu 1", + Label: "Checkbox Menu 1", Type: menu.CheckboxType, Accelerator: menu.CmdOrCtrlAccel("l"), Checked: true, }, + menu.Checkbox("Checkbox Menu 2", "checkbox-menu 2", false), menu.Separator(), menu.Radio("๐Ÿ˜€ Option 1", "๐Ÿ˜€option-1", true), menu.Radio("๐Ÿ˜บ Option 2", "option-2", false),