diff --git a/v2/internal/messagedispatcher/message/messageparser.go b/v2/internal/messagedispatcher/message/messageparser.go index a8c0dd5a7..e76aeca26 100644 --- a/v2/internal/messagedispatcher/message/messageparser.go +++ b/v2/internal/messagedispatcher/message/messageparser.go @@ -19,6 +19,7 @@ var messageParsers = map[byte]func(string) (*parsedMessage, error){ 'D': dialogMessageParser, 'S': systemMessageParser, 'M': menuMessageParser, + 'T': trayMessageParser, } // Parse will attempt to parse the given message diff --git a/v2/internal/messagedispatcher/message/tray.go b/v2/internal/messagedispatcher/message/tray.go new file mode 100644 index 000000000..76739e2cc --- /dev/null +++ b/v2/internal/messagedispatcher/message/tray.go @@ -0,0 +1,43 @@ +package message + +import ( + "fmt" + + "github.com/wailsapp/wails/v2/pkg/menu" +) + +// TrayOnMessage is used to emit listener registration requests +// on the service bus +type TrayOnMessage struct { + // MenuID is the id of the menu item we are interested in + MenuID string + // Callback is called when the menu is clicked + Callback func(*menu.MenuItem) +} + +// trayMessageParser does what it says on the tin! +func trayMessageParser(message string) (*parsedMessage, error) { + + // Sanity check: Menu messages must be at least 2 bytes + if len(message) < 3 { + return nil, fmt.Errorf("event message was an invalid length") + } + + var topic string + var data interface{} + + // Switch the message type + switch message[1] { + case 'C': + callbackid := message[2:] + topic = "tray:clicked" + data = callbackid + default: + return nil, fmt.Errorf("invalid menu message: %s", message) + } + + // Create a new parsed message struct + parsedMessage := &parsedMessage{Topic: topic, Data: data} + + return parsedMessage, nil +} diff --git a/v2/internal/runtime/runtime.go b/v2/internal/runtime/runtime.go index f518cb4e6..6b33214cb 100644 --- a/v2/internal/runtime/runtime.go +++ b/v2/internal/runtime/runtime.go @@ -13,6 +13,7 @@ type Runtime struct { Dialog Dialog System System Menu Menu + Tray Tray Store *StoreProvider Log Log bus *servicebus.ServiceBus @@ -27,6 +28,7 @@ func New(serviceBus *servicebus.ServiceBus, menu *menu.Menu) *Runtime { Dialog: newDialog(serviceBus), System: newSystem(serviceBus), Menu: newMenu(serviceBus, menu), + Tray: newTray(serviceBus, menu), Log: newLog(serviceBus), bus: serviceBus, } diff --git a/v2/internal/runtime/tray.go b/v2/internal/runtime/tray.go new file mode 100644 index 000000000..b3c3ab6f0 --- /dev/null +++ b/v2/internal/runtime/tray.go @@ -0,0 +1,48 @@ +package runtime + +import ( + "github.com/wailsapp/wails/v2/internal/messagedispatcher/message" + "github.com/wailsapp/wails/v2/internal/servicebus" + "github.com/wailsapp/wails/v2/pkg/menu" +) + +// Tray defines all Tray related operations +type Tray interface { + On(menuID string, callback func(*menu.MenuItem)) + Update() + GetByID(menuID string) *menu.MenuItem + RemoveByID(id string) bool +} + +type trayRuntime struct { + bus *servicebus.ServiceBus + trayMenu *menu.Menu +} + +// newTray creates a new Menu struct +func newTray(bus *servicebus.ServiceBus, menu *menu.Menu) Menu { + return &trayRuntime{ + bus: bus, + trayMenu: menu, + } +} + +// On registers a listener for a particular event +func (t *trayRuntime) On(menuID string, callback func(*menu.MenuItem)) { + t.bus.Publish("tray:on", &message.TrayOnMessage{ + MenuID: menuID, + Callback: callback, + }) +} + +func (t *trayRuntime) Update() { + t.bus.Publish("tray:update", t.trayMenu) +} + +func (t *trayRuntime) GetByID(menuID string) *menu.MenuItem { + return t.trayMenu.GetByID(menuID) +} + +func (t *trayRuntime) RemoveByID(id string) bool { + return t.trayMenu.RemoveByID(id) +} diff --git a/v2/internal/subsystem/tray.go b/v2/internal/subsystem/tray.go index 1e15fe1af..cdccbadb4 100644 --- a/v2/internal/subsystem/tray.go +++ b/v2/internal/subsystem/tray.go @@ -87,7 +87,7 @@ func (t *Tray) Start() error { t.logger.Error("Received clicked message with invalid topic format. Expected 2 sections in topic, got %s", splitTopic) continue } - t.logger.Trace("Got Menu clicked Message: %s %+v", menuMessage.Topic(), menuMessage.Data()) + t.logger.Trace("Got Tray Menu clicked Message: %s %+v", menuMessage.Topic(), menuMessage.Data()) menuid := menuMessage.Data().(string) // Get the menu item @@ -106,7 +106,7 @@ func (t *Tray) Start() error { // Notify listeners t.notifyListeners(menuid, menuItem) case "on": - listenerDetails := menuMessage.Data().(*message.MenuOnMessage) + listenerDetails := menuMessage.Data().(*message.TrayOnMessage) id := listenerDetails.MenuID t.listeners[id] = append(t.listeners[id], listenerDetails.Callback) diff --git a/v2/test/kitchensink/main.go b/v2/test/kitchensink/main.go index ff879bcdf..62af966c4 100644 --- a/v2/test/kitchensink/main.go +++ b/v2/test/kitchensink/main.go @@ -39,6 +39,7 @@ func main() { app.Bind(&Dialog{}) app.Bind(&Window{}) app.Bind(&Menu{}) + app.Bind(&Tray{}) err = app.Run() if err != nil { diff --git a/v2/test/kitchensink/menu.go b/v2/test/kitchensink/menu.go index 03fb6645f..d8902b2b6 100644 --- a/v2/test/kitchensink/menu.go +++ b/v2/test/kitchensink/menu.go @@ -213,12 +213,6 @@ func (m *Menu) insertAfterRandom(_ *menu.MenuItem) { m.runtime.Menu.Update() } -func createApplicationTray() *menu.Menu { - trayMenu := &menu.Menu{} - trayMenu.Append(menu.Text("Hello from the tray!", "hi")) - return trayMenu -} - func createApplicationMenu() *menu.Menu { // Create menu diff --git a/v2/test/kitchensink/tray.go b/v2/test/kitchensink/tray.go new file mode 100644 index 000000000..2f8260486 --- /dev/null +++ b/v2/test/kitchensink/tray.go @@ -0,0 +1,114 @@ +package main + +import ( + "strconv" + "sync" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/menu" +) + +// Tray struct +type Tray struct { + runtime *wails.Runtime + + dynamicMenuCounter int + lock sync.Mutex + dynamicMenuItems map[string]*menu.MenuItem + anotherDynamicMenuCounter int +} + +// WailsInit is called at application startup +func (t *Tray) WailsInit(runtime *wails.Runtime) error { + // Perform your setup here + t.runtime = runtime + + // Setup Menu Listeners + t.runtime.Tray.On("Show Window", func(mi *menu.MenuItem) { + t.runtime.Window.Show() + }) + t.runtime.Tray.On("Hide Window", func(mi *menu.MenuItem) { + t.runtime.Window.Hide() + }) + + return nil +} + +func (t *Tray) incrementcounter() int { + t.dynamicMenuCounter++ + return t.dynamicMenuCounter +} + +func (t *Tray) decrementcounter() int { + t.dynamicMenuCounter-- + return t.dynamicMenuCounter +} + +func (t *Tray) addMenu(mi *menu.MenuItem) { + + // Lock because this method will be called in a gorouting + t.lock.Lock() + defer t.lock.Unlock() + + // Get this menu's parent + parent := mi.Parent() + counter := t.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) + t.runtime.Tray.On("Remove Last Item", t.removeMenu) + } else { + removeMenu := t.runtime.Tray.GetByID("Remove Last Item") + // Test if the remove menu hasn't already been removed in another thread + if removeMenu != nil { + removeMenu.Label = "Remove " + menuText + } + } + t.runtime.Tray.Update() +} + +func (t *Tray) removeMenu(_ *menu.MenuItem) { + + // Lock because this method will be called in a goroutine + t.lock.Lock() + defer t.lock.Unlock() + + // Get the id of the last dynamic menu + menuID := "Dynamic Menu Item " + strconv.Itoa(t.dynamicMenuCounter) + + // Remove the last menu item by ID + t.runtime.Tray.RemoveByID(menuID) + + // Update the counter + counter := t.decrementcounter() + + // If we deleted the last dynamic menu, remove the "Remove Last Item" menu + if counter == 0 { + t.runtime.Tray.RemoveByID("Remove Last Item") + } else { + // Update label + menuText := "Dynamic Menu Item " + strconv.Itoa(counter) + removeMenu := t.runtime.Tray.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("["))) + t.runtime.Tray.Update() +} + +func createApplicationTray() *menu.Menu { + trayMenu := &menu.Menu{} + trayMenu.Append(menu.Text("Show Window", "Show Window")) + trayMenu.Append(menu.Text("Hide Window", "Hide Window")) + return trayMenu +}