diff --git a/v2/internal/app/desktop.go b/v2/internal/app/desktop.go index 50d6fe5d8..2f33ad69d 100644 --- a/v2/internal/app/desktop.go +++ b/v2/internal/app/desktop.go @@ -33,6 +33,7 @@ type App struct { binding *subsystem.Binding call *subsystem.Call menu *subsystem.Menu + tray *subsystem.Tray dispatcher *messagedispatcher.Dispatcher // Indicates if the app is in debug mode @@ -47,7 +48,7 @@ type App struct { } // Create App -func CreateApp(options *options.App) *App { +func CreateApp(options *options.App) (*App, error) { // Merge default options options.MergeDefaults() @@ -68,15 +69,17 @@ func CreateApp(options *options.App) *App { result.options = options // Initialise the app - result.Init() + err := result.Init() - return result + return result, err } // Run the application func (a *App) Run() error { + var err error + // Setup signal handler signalsubsystem, err := signal.NewManager(a.servicebus, a.logger) if err != nil { @@ -87,7 +90,10 @@ func (a *App) Run() error { // Start the service bus a.servicebus.Debug() - a.servicebus.Start() + err = a.servicebus.Start() + if err == nil { + return err + } // Start the runtime runtimesubsystem, err := subsystem.NewRuntime(a.servicebus, a.logger, @@ -96,7 +102,10 @@ func (a *App) Run() error { return err } a.runtime = runtimesubsystem - a.runtime.Start() + err = a.runtime.Start() + if err == nil { + return err + } // Application Stores a.loglevelStore = a.runtime.GoRuntime().Store.New("wails:loglevel", a.options.LogLevel) @@ -109,7 +118,10 @@ func (a *App) Run() error { return err } a.binding = bindingsubsystem - a.binding.Start() + err = a.binding.Start() + if err == nil { + return err + } // Start the logging subsystem log, err := subsystem.NewLog(a.servicebus, a.logger, a.loglevelStore) @@ -117,7 +129,10 @@ func (a *App) Run() error { return err } a.log = log - a.log.Start() + err = a.log.Start() + if err == nil { + return err + } // create the dispatcher dispatcher, err := messagedispatcher.New(a.servicebus, a.logger) @@ -125,7 +140,10 @@ func (a *App) Run() error { return err } a.dispatcher = dispatcher - dispatcher.Start() + err = dispatcher.Start() + if err == nil { + return err + } // Start the eventing subsystem event, err := subsystem.NewEvent(a.servicebus, a.logger) @@ -133,26 +151,53 @@ func (a *App) Run() error { return err } a.event = event - a.event.Start() + err = a.event.Start() + if err == nil { + return err + } // Start the menu subsystem - var platformMenu *menu.Menu + var applicationMenu *menu.Menu + var trayMenu *menu.Menu switch goruntime.GOOS { case "darwin": - platformMenu = a.options.Mac.Menu + applicationMenu = a.options.Mac.Menu + trayMenu = a.options.Mac.Tray // case "linux": - // platformMenu = a.options.Linux.Menu + // applicationMenu = a.options.Linux.Menu // case "windows": - // platformMenu = a.options.Windows.Menu + // applicationMenu = a.options.Windows.Menu default: return fmt.Errorf("unsupported OS: %s", goruntime.GOOS) } - menusubsystem, err := subsystem.NewMenu(platformMenu, a.servicebus, a.logger) - if err != nil { - return err + + // Optionally start the menu subsystem + if applicationMenu != nil { + menusubsystem, err := subsystem.NewMenu(applicationMenu, a.servicebus, + a.logger) + if err != nil { + return err + } + a.menu = menusubsystem + err = a.menu.Start() + if err == nil { + return err + } + } + + // Optionally start the tray subsystem + if trayMenu != nil { + traysubsystem, err := subsystem.NewTray(trayMenu, a.servicebus, + a.logger) + if err != nil { + return err + } + a.tray = traysubsystem + err = a.tray.Start() + if err == nil { + return err + } } - a.menu = menusubsystem - a.menu.Start() // Start the call subsystem call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB(), a.runtime.GoRuntime()) @@ -160,7 +205,10 @@ func (a *App) Run() error { return err } a.call = call - a.call.Start() + err = a.call.Start() + if err == nil { + return err + } // Dump bindings as a debug bindingDump, err := a.bindings.ToJSON() @@ -170,7 +218,10 @@ func (a *App) Run() error { result := a.window.Run(dispatcher, bindingDump, a.debug) a.logger.Trace("Ffenestri.Run() exited") - a.servicebus.Stop() + err = a.servicebus.Stop() + if err == nil { + return err + } return result } diff --git a/v2/internal/ffenestri/ffenestri_darwin.c b/v2/internal/ffenestri/ffenestri_darwin.c index ca3d46825..1362902bf 100644 --- a/v2/internal/ffenestri/ffenestri_darwin.c +++ b/v2/internal/ffenestri/ffenestri_darwin.c @@ -74,12 +74,18 @@ extern const char *icon[]; // MAIN DEBUG FLAG int debug; -// MenuItem map +// MenuItem map for the application menu struct hashmap_s menuItemMapForApplicationMenu; -// RadioGroup map. Maps a menuitem id with its associated radio group items +// RadioGroup map for the application menu. Maps a menuitem id with its associated radio group items struct hashmap_s radioGroupMapForApplicationMenu; +// MenuItem map for the tray menu +struct hashmap_s menuItemMapForTrayMenu; + +// RadioGroup map for the tray menu. Maps a menuitem id with its associated radio group items +struct hashmap_s radioGroupMapForTrayMenu; + // Dispatch Method typedef void (^dispatchMethod)(void); @@ -341,7 +347,7 @@ void messageHandler(id self, SEL cmd, id contentController, id message) { } // Callback for menu items -void menuItemPressed(id self, SEL cmd, id sender) { +void menuItemPressedForApplicationMenu(id self, SEL cmd, id sender) { const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); // Notify the backend const char *message = concat("MC", menuID); @@ -349,10 +355,17 @@ void menuItemPressed(id self, SEL cmd, id sender) { free((void*)message); } +// Callback for tray items +void menuItemPressedForTrayMenu(id self, SEL cmd, id sender) { + const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); + // Notify the backend + const char *message = concat("TC", menuID); + messageFromWindowCallback(message); + free((void*)message); +} + // Callback for menu items -void checkboxMenuItemPressedForApplicationMenu(id self, SEL cmd, id sender, struct -hashmap_s -*menuItemMap) { +void checkboxMenuItemPressedForApplicationMenu(id self, SEL cmd, id sender, struct hashmap_s *menuItemMap) { const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); // Get the menu item from the menu item map @@ -370,7 +383,26 @@ hashmap_s free((void*)message); } -// radioMenuItemPressed +// Callback for tray menu items +void checkboxMenuItemPressedForTrayMenu(id self, SEL cmd, id sender, struct hashmap_s *menuItemMap) { + const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); + + // Get the menu item from the menu item map + id menuItem = (id)hashmap_get(&menuItemMapForTrayMenu, (char*)menuID, strlen(menuID)); + + // Get the current state + bool state = msg(menuItem, s("state")); + + // Toggle the state + msg(menuItem, s("setState:"), (state? NSControlStateValueOff : NSControlStateValueOn)); + + // Notify the backend + const char *message = concat("TC", menuID); + messageFromWindowCallback(message); + free((void*)message); +} + +// radioMenuItemPressedForApplicationMenu void radioMenuItemPressedForApplicationMenu(id self, SEL cmd, id sender) { const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); @@ -406,6 +438,43 @@ void radioMenuItemPressedForApplicationMenu(id self, SEL cmd, id sender) { free((void*)message); } + +// radioMenuItemPressedForTrayMenu +void radioMenuItemPressedForTrayMenu(id self, SEL cmd, id sender) { + const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); + + // Get the menu item from the menu item map + id menuItem = (id)hashmap_get(&menuItemMapForTrayMenu, (char*)menuID, strlen(menuID)); + + // Check the menu items' current state + bool selected = msg(menuItem, s("state")); + + // If it's already selected, exit early + if (selected) { + return; + } + + // Get this item's radio group members and turn them off + id *members = (id*)hashmap_get(&radioGroupMapForTrayMenu, (char*)menuID, strlen(menuID)); + + // Uncheck all members of the group + id thisMember = members[0]; + int count = 0; + while(thisMember != NULL) { + msg(thisMember, s("setState:"), NSControlStateValueOff); + count = count + 1; + thisMember = members[count]; + } + + // check the selected menu item + msg(menuItem, s("setState:"), NSControlStateValueOn); + + // Notify the backend + const char *message = concat("TC", menuID); + messageFromWindowCallback(message); + free((void*)message); +} + // closeWindow is called when the close button is pressed void closeWindow(id self, SEL cmd, id sender) { struct Application *app = (struct Application *) objc_getAssociatedObject(self, "application"); @@ -459,6 +528,22 @@ void allocateMenuHashMaps(struct Application *app) { } } +void allocateTrayHashMaps(struct Application *app) { + // Allocate new menuItem map + if( 0 != hashmap_create((const unsigned)16, &menuItemMapForTrayMenu)) { + // Couldn't allocate map + Fatal(app, "Not enough memory to allocate menuItemMapForTrayMenu!"); + return; + } + + // Allocate the Radio Group Cache + if( 0 != hashmap_create((const unsigned)4, &radioGroupMapForTrayMenu)) { + // Couldn't allocate map + Fatal(app, "Not enough memory to allocate radioGroupMapForTrayMenu!"); + return; + } +} + void* NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel) { // Setup main application struct struct Application *result = malloc(sizeof(struct Application)); @@ -548,21 +633,45 @@ void destroyMenu(struct Application *app) { json_delete(app->processedMenu); app->processedMenu = NULL; } - - // Release the tray menu json if we have it - if ( app->trayMenuAsJSON != NULL ) { - free((void*)app->trayMenuAsJSON); - app->trayMenuAsJSON = NULL; - } - - // Release processed menu - if( app->processedTrayMenu != NULL) { - json_delete(app->processedTrayMenu); - app->processedTrayMenu = NULL; - } } +void destroyTray(struct Application *app) { + + // If we don't have a tray, exit! + if( app->trayMenuAsJSON == NULL ) { + return; + } + + // Free menu item hashmap + hashmap_destroy(&menuItemMapForTrayMenu); + + // Free radio group members + if( hashmap_num_entries(&radioGroupMapForTrayMenu) > 0 ) { + if (0!=hashmap_iterate_pairs(&radioGroupMapForTrayMenu, freeHashmapItem, NULL)) { + Fatal(app, "failed to deallocate hashmap entries!"); + } + } + + //Free radio groups hashmap + hashmap_destroy(&radioGroupMapForTrayMenu); + + // Release the menu json if we have it + if ( app->trayMenuAsJSON != NULL ) { + free((void*)app->trayMenuAsJSON); + app->trayMenuAsJSON = NULL; + } + + // Release processed tray + if( app->processedTrayMenu != NULL) { + json_delete(app->processedTrayMenu); + app->processedTrayMenu = NULL; + } +} + + + + void DestroyApplication(struct Application *app) { Debug(app, "Destroying Application"); @@ -582,8 +691,12 @@ void DestroyApplication(struct Application *app) { msg( c("NSEvent"), s("removeMonitor:"), app->mouseUpMonitor); } + // Destroy the menu destroyMenu(app); + // Destroy the tray + destroyTray(app); + // Remove script handlers msg(app->manager, s("removeScriptMessageHandlerForName:"), str("windowDrag")); msg(app->manager, s("removeScriptMessageHandlerForName:"), str("external")); @@ -1028,11 +1141,12 @@ void createDelegate(struct Application *app) { class_addMethod(delegateClass, s("applicationWillTerminate:"), (IMP) closeWindow, "v@:@"); // Menu Callbacks - class_addMethod(delegateClass, s("menuCallback:"), (IMP)menuItemPressed, "v@:@"); - class_addMethod(delegateClass, s("checkboxMenuCallbackForApplicationMenu:"), (IMP)checkboxMenuItemPressedForApplicationMenu, "v@:@"); - class_addMethod(delegateClass, s("radioMenuCallbackForApplicationMenu:"), - (IMP) - radioMenuItemPressedForApplicationMenu, "v@:@"); + class_addMethod(delegateClass, s("menuCallbackForApplicationMenu:"), (IMP)menuItemPressedForApplicationMenu, "v@:@"); + class_addMethod(delegateClass, s("checkboxMenuCallbackForApplicationMenu:"), (IMP) checkboxMenuItemPressedForApplicationMenu, "v@:@"); + class_addMethod(delegateClass, s("radioMenuCallbackForApplicationMenu:"), (IMP) radioMenuItemPressedForApplicationMenu, "v@:@"); + class_addMethod(delegateClass, s("menuCallbackForTrayMenu:"), (IMP)menuItemPressedForTrayMenu, "v@:@"); + class_addMethod(delegateClass, s("checkboxMenuCallbackForTrayMenu:"), (IMP) checkboxMenuItemPressedForTrayMenu, "v@:@"); + class_addMethod(delegateClass, s("radioMenuCallbackForTrayMenu:"), (IMP) radioMenuItemPressedForTrayMenu, "v@:@"); // Script handler @@ -1476,14 +1590,14 @@ id processAcceleratorKey(const char *key) { } -id parseTextMenuItem(struct Application *app, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers) { +id parseTextMenuItem(struct Application *app, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char *menuCallback) { id item = ALLOC("NSMenuItem"); id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), menuid); msg(item, s("setRepresentedObject:"), wrappedId); id key = processAcceleratorKey(acceleratorkey); msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), - s("menuCallback:"), key); + s(menuCallback), key); msg(item, s("setEnabled:"), !disabled); msg(item, s("autorelease")); @@ -1542,7 +1656,7 @@ id parseRadioMenuItem(struct Application *app, id parentmenu, const char *title, void parseMenuItem(struct Application *app, id parentMenu, JsonNode *item, struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char -*radioCallbackFunction) { +*radioCallbackFunction, const char *menuCallbackFunction) { // Check if this item is hidden and if so, exit early! bool hidden = false; @@ -1578,8 +1692,7 @@ struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char JsonNode *item; json_foreach(item, submenu) { // Get item label - parseMenuItem(app, thisMenu, item, menuItemMap, checkboxCallbackFunction, radioCallbackFunction - ); + parseMenuItem(app, thisMenu, item, menuItemMap, checkboxCallbackFunction, radioCallbackFunction, menuCallbackFunction); } return; @@ -1634,7 +1747,7 @@ struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char if( type != NULL ) { if( STREQ(type->string_, "Text")) { - parseTextMenuItem(app, parentMenu, label, menuid, disabled, acceleratorkey, modifiers); + parseTextMenuItem(app, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, menuCallbackFunction); } else if ( STREQ(type->string_, "Separator")) { addSeparator(parentMenu); @@ -1644,16 +1757,14 @@ struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char bool checked = false; getJSONBool(item, "Checked", &checked); - parseCheckboxMenuItem(app, parentMenu, label, menuid, disabled, checked, - "", menuItemMap, checkboxCallbackFunction); + parseCheckboxMenuItem(app, parentMenu, label, menuid, disabled, checked, "", menuItemMap, checkboxCallbackFunction); } else if ( STREQ(type->string_, "Radio")) { // Get checked state bool checked = false; getJSONBool(item, "Checked", &checked); - parseRadioMenuItem(app, parentMenu, label, menuid, disabled, checked, "", - menuItemMap, radioCallbackFunction); + parseRadioMenuItem(app, parentMenu, label, menuid, disabled, checked, "", menuItemMap, radioCallbackFunction); } if ( modifiers != NULL ) { @@ -1664,8 +1775,7 @@ struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char } } -void parseMenu(struct Application *app, id parentMenu, JsonNode *menu, struct -hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char *radioCallbackFunction) { +void parseMenu(struct Application *app, id parentMenu, JsonNode *menu, struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char *radioCallbackFunction, const char *menuCallbackFunction) { JsonNode *items = json_find_member(menu, "Items"); if( items == NULL ) { // Parse error! @@ -1677,8 +1787,7 @@ hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char *radioC JsonNode *item; json_foreach(item, items) { // Get item label - parseMenuItem(app, parentMenu, item, menuItemMap, checkboxCallbackFunction, radioCallbackFunction - ); + parseMenuItem(app, parentMenu, item, menuItemMap, checkboxCallbackFunction, radioCallbackFunction, menuCallbackFunction); } } @@ -1764,7 +1873,7 @@ void parseMenuData(struct Application *app) { parseMenu(app, menubar, menuData, &menuItemMapForApplicationMenu, - "checkboxMenuCallbackForApplicationMenu:", "radioMenuCallbackForApplicationMenu:"); + "checkboxMenuCallbackForApplicationMenu:", "radioMenuCallbackForApplicationMenu:", "menuCallbackForApplicationMenu:"); // Create the radiogroup cache JsonNode *radioGroups = json_find_member(app->processedMenu, "RadioGroups"); @@ -1807,17 +1916,64 @@ void UpdateMenu(struct Application *app, const char *menuAsJSON) { } void parseTrayData(struct Application *app) { - id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") ); - id statusItem = msg(statusBar, s("statusItemWithLength:"), -1.0); - msg(statusItem, s("retain")); - id statusBarButton = msg(statusItem, s("button")); // msg(statusBarButton, s("setImage:"), // msg(c("NSImage"), s("imageNamed:"), // msg(c("NSString"), s("stringWithUTF8String:"), tray->icon))); - msg(statusItem, s("setMenu:"), NULL); + // Allocate the hashmaps we need + allocateTrayHashMaps(app); + + // Create a new menu + id traymenu = createMenu(str("")); + + // Create a new menu bar + id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") ); + id statusItem = msg(statusBar, s("statusItemWithLength:"), -1.0); + msg(statusItem, s("retain")); + id statusBarButton = msg(statusItem, s("button")); + + Debug(app, ">>>>>>>>>>> TRAY MENU: %s", app->trayMenuAsJSON); + + // Parse the processed menu json + app->processedTrayMenu = json_decode(app->trayMenuAsJSON); + + if( app->processedTrayMenu == NULL ) { + // Parse error! + Fatal(app, "Unable to parse Tray JSON: %s", app->trayMenuAsJSON); + return; + } + + + // Pull out the Menu + JsonNode *trayMenuData = json_find_member(app->processedTrayMenu, "Menu"); + if( trayMenuData == NULL ) { + // Parse error! + Fatal(app, "Unable to find Menu data: %s", app->processedTrayMenu); + return; + } + + + parseMenu(app, traymenu, trayMenuData, &menuItemMapForTrayMenu, + "checkboxMenuCallbackForTrayMenu:", "radioMenuCallbackForTrayMenu:", "menuCallbackForTrayMenu:"); + + // Create the radiogroup cache + JsonNode *radioGroups = json_find_member(app->processedTrayMenu, "RadioGroups"); + if( radioGroups == NULL ) { + // Parse error! + Fatal(app, "Unable to find RadioGroups data: %s", app->processedTrayMenu); + return; + } + + // Iterate radio groups + JsonNode *radioGroup; + json_foreach(radioGroup, radioGroups) { + // Get item label + processRadioGroup(radioGroup, &menuItemMapForTrayMenu, &radioGroupMapForTrayMenu); + } + + msg(statusItem, s("setMenu:"), traymenu); } @@ -1987,9 +2143,13 @@ void Run(struct Application *app, int argc, char **argv) { } // If we have a tray menu, process it + printf + ("\n\n\n*****************************************************************************************************************************************************************************************************************\n\n\n"); if( app->trayMenuAsJSON != NULL ) { parseTrayData(app); } +printf + ("\n\n\n*****************************************************************************************************************************************************************************************************************\n\n\n"); // Finally call run Debug(app, "Run called"); diff --git a/v2/internal/ffenestri/ffenestri_darwin.go b/v2/internal/ffenestri/ffenestri_darwin.go index dbca5017f..d70986e84 100644 --- a/v2/internal/ffenestri/ffenestri_darwin.go +++ b/v2/internal/ffenestri/ffenestri_darwin.go @@ -15,6 +15,7 @@ extern void SetAppearance(void *, const char *); extern void WebviewIsTransparent(void *); extern void SetWindowBackgroundIsTranslucent(void *); extern void SetMenu(void *, const char *); +extern void SetTray(void *, const char *); */ import "C" import ( @@ -79,27 +80,33 @@ func (a *Application) processPlatformSettings() error { We keep a record of every radio group member we discover by saving a list of all members of the group and the number of members in the group (this last one is for optimisation at the C layer). - - Example: - { - "RadioGroups": [ - { - "Members": [ - "option-1", - "option-2", - "option-3" - ], - "Length": 3 - } - ] - } */ processedMenu := NewProcessedMenu(mac.Menu) - menuJSON, err := json.Marshal(processedMenu) + applicationMenuJSON, err := json.Marshal(processedMenu) if err != nil { return err } - C.SetMenu(a.app, a.string2CString(string(menuJSON))) + C.SetMenu(a.app, a.string2CString(string(applicationMenuJSON))) + } + + // Process tray + if mac.Tray != nil { + + /* + As radio groups need to be manually managed on OSX, + we preprocess the menu to determine the radio groups. + This is defined as any adjacent menu item of type "RadioType". + We keep a record of every radio group member we discover by saving + a list of all members of the group and the number of members + in the group (this last one is for optimisation at the C layer). + */ + processedMenu := NewProcessedMenu(mac.Tray) + trayMenuJSON, err := json.Marshal(processedMenu) + if err != nil { + return err + } + C.SetTray(a.app, a.string2CString(string(trayMenuJSON))) + println("******************** SET TRAY!!!!! &&&&&&&&&&&&&&&&&&&&&&&&&&") } return nil diff --git a/v2/internal/subsystem/tray.go b/v2/internal/subsystem/tray.go new file mode 100644 index 000000000..1e15fe1af --- /dev/null +++ b/v2/internal/subsystem/tray.go @@ -0,0 +1,186 @@ +package subsystem + +import ( + "strings" + "sync" + + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/internal/messagedispatcher/message" + "github.com/wailsapp/wails/v2/internal/servicebus" + "github.com/wailsapp/wails/v2/pkg/menu" +) + +// Tray is the subsystem that handles the operation of the tray menu. +// It manages all service bus messages starting with "tray". +type Tray struct { + quitChannel <-chan *servicebus.Message + trayChannel <-chan *servicebus.Message + running bool + + // Event listeners + listeners map[string][]func(*menu.MenuItem) + menuItems map[string]*menu.MenuItem + notifyLock sync.RWMutex + + // logger + logger logger.CustomLogger + + // The tray menu + trayMenu *menu.Menu + + // Service Bus + bus *servicebus.ServiceBus +} + +// NewTray creates a new menu subsystem +func NewTray(trayMenu *menu.Menu, bus *servicebus.ServiceBus, + logger *logger.Logger) (*Tray, error) { + + // Register quit channel + quitChannel, err := bus.Subscribe("quit") + if err != nil { + return nil, err + } + + // Subscribe to menu messages + trayChannel, err := bus.Subscribe("tray:") + if err != nil { + return nil, err + } + + result := &Tray{ + quitChannel: quitChannel, + trayChannel: trayChannel, + logger: logger.CustomLogger("Tray Subsystem"), + listeners: make(map[string][]func(*menu.MenuItem)), + menuItems: make(map[string]*menu.MenuItem), + trayMenu: trayMenu, + bus: bus, + } + + // Build up list of item/id pairs + result.processMenu(trayMenu) + + return result, nil +} + +// Start the subsystem +func (t *Tray) Start() error { + + t.logger.Trace("Starting") + + t.running = true + + // Spin off a go routine + go func() { + for t.running { + select { + case <-t.quitChannel: + t.running = false + break + case menuMessage := <-t.trayChannel: + splitTopic := strings.Split(menuMessage.Topic(), ":") + menuMessageType := splitTopic[1] + switch menuMessageType { + case "clicked": + if len(splitTopic) != 2 { + 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()) + menuid := menuMessage.Data().(string) + + // Get the menu item + menuItem := t.menuItems[menuid] + if menuItem == nil { + t.logger.Trace("Cannot process menuid %s - unknown", menuid) + return + } + + // Is the menu item a checkbox? + if menuItem.Type == menu.CheckboxType { + // Toggle state + menuItem.Checked = !menuItem.Checked + } + + // Notify listeners + t.notifyListeners(menuid, menuItem) + case "on": + listenerDetails := menuMessage.Data().(*message.MenuOnMessage) + id := listenerDetails.MenuID + t.listeners[id] = append(t.listeners[id], listenerDetails.Callback) + + // Make sure we catch any menu updates + case "update": + updatedMenu := menuMessage.Data().(*menu.Menu) + t.processMenu(updatedMenu) + + // Notify frontend of menu change + t.bus.Publish("trayfrontend:update", updatedMenu) + + default: + t.logger.Error("unknown tray message: %+v", menuMessage) + } + } + } + + // Call shutdown + t.shutdown() + }() + + return nil +} + +func (t *Tray) processMenu(trayMenu *menu.Menu) { + // Initialise the variables + t.menuItems = make(map[string]*menu.MenuItem) + t.trayMenu = trayMenu + + for _, item := range trayMenu.Items { + t.processMenuItem(item) + } +} + +func (t *Tray) processMenuItem(item *menu.MenuItem) { + + if item.SubMenu != nil { + for _, submenuitem := range item.SubMenu { + t.processMenuItem(submenuitem) + } + return + } + + if item.ID != "" { + if t.menuItems[item.ID] != nil { + t.logger.Error("Menu id '%s' is used by multiple menu items: %s %s", t.menuItems[item.ID].Label, item.Label) + return + } + t.menuItems[item.ID] = item + } +} + +// Notifies listeners that the given menu was clicked +func (t *Tray) notifyListeners(menuid string, menuItem *menu.MenuItem) { + + // Get list of menu listeners + listeners := t.listeners[menuid] + if listeners == nil { + t.logger.Trace("No listeners for MenuItem with ID '%s'", menuid) + return + } + + // Lock the listeners + t.notifyLock.Lock() + + // Callback in goroutine + for _, listener := range listeners { + go listener(menuItem) + } + + // Unlock + t.notifyLock.Unlock() +} + +func (t *Tray) shutdown() { + t.logger.Trace("Shutdown") +} diff --git a/v2/pkg/options/mac/mac.go b/v2/pkg/options/mac/mac.go index 05b10d3a2..db9495826 100644 --- a/v2/pkg/options/mac/mac.go +++ b/v2/pkg/options/mac/mac.go @@ -9,4 +9,5 @@ type Options struct { WebviewIsTransparent bool WindowBackgroundIsTranslucent bool Menu *menu.Menu + Tray *menu.Menu } diff --git a/v2/test/kitchensink/main.go b/v2/test/kitchensink/main.go index 333ef0766..299dcf9cc 100644 --- a/v2/test/kitchensink/main.go +++ b/v2/test/kitchensink/main.go @@ -1,10 +1,11 @@ package main import ( - wails "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/logger" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/mac" + "log" ) func main() { @@ -22,6 +23,7 @@ func main() { // Comment out line below to see Window.SetTitle() work TitleBar: mac.TitleBarHiddenInset(), Menu: createApplicationMenu(), + Tray: createApplicationTray(), }, LogLevel: logger.TRACE, }) @@ -34,5 +36,8 @@ func main() { app.Bind(&Window{}) app.Bind(&Menu{}) - app.Run() + err := app.Run() + if err != nil { + log.Fatal(err) + } } diff --git a/v2/test/kitchensink/menu.go b/v2/test/kitchensink/menu.go index faa51ac19..03fb6645f 100644 --- a/v2/test/kitchensink/menu.go +++ b/v2/test/kitchensink/menu.go @@ -6,7 +6,7 @@ import ( "strconv" "sync" - wails "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/menu" ) @@ -213,6 +213,12 @@ 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