diff --git a/v2/internal/app/default.go b/v2/internal/app/default.go index cc1282981..26dce8960 100644 --- a/v2/internal/app/default.go +++ b/v2/internal/app/default.go @@ -25,7 +25,7 @@ type App struct { } // CreateApp returns a null application -func CreateApp(options *options.App) *App { +func CreateApp(_ *options.App) *App { return &App{} } @@ -37,6 +37,5 @@ func (a *App) Run() error { } // Bind the dummy interface -func (a *App) Bind(dummy interface{}) error { - return nil +func (a *App) Bind(_ interface{}) { } diff --git a/v2/internal/app/desktop.go b/v2/internal/app/desktop.go index abefe1437..3548fb3d4 100644 --- a/v2/internal/app/desktop.go +++ b/v2/internal/app/desktop.go @@ -23,14 +23,15 @@ type App struct { options *options.App // Subsystems - log *subsystem.Log - runtime *subsystem.Runtime - event *subsystem.Event - binding *subsystem.Binding - call *subsystem.Call - menu *subsystem.Menu - tray *subsystem.Tray - dispatcher *messagedispatcher.Dispatcher + log *subsystem.Log + runtime *subsystem.Runtime + event *subsystem.Event + binding *subsystem.Binding + call *subsystem.Call + menu *subsystem.Menu + tray *subsystem.Tray + contextmenus *subsystem.ContextMenus + dispatcher *messagedispatcher.Dispatcher // Indicates if the app is in debug mode debug bool @@ -94,8 +95,9 @@ func (a *App) Run() error { // Start the runtime applicationMenu := options.GetApplicationMenu(a.options) trayMenu := options.GetTrayMenu(a.options) + contextMenus := options.GetContextMenus(a.options) - runtimesubsystem, err := subsystem.NewRuntime(a.servicebus, a.logger, applicationMenu, trayMenu) + runtimesubsystem, err := subsystem.NewRuntime(a.servicebus, a.logger, applicationMenu, trayMenu, contextMenus) if err != nil { return err } @@ -179,6 +181,19 @@ func (a *App) Run() error { } } + // Optionally start the context menu subsystem + if contextMenus != nil { + contextmenussubsystem, err := subsystem.NewContextMenus(contextMenus, a.servicebus, a.logger) + if err != nil { + return err + } + a.contextmenus = contextmenussubsystem + err = a.contextmenus.Start() + if err != nil { + return err + } + } + // Start the call subsystem call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB(), a.runtime.GoRuntime()) if err != nil { diff --git a/v2/internal/ffenestri/ffenestri.h b/v2/internal/ffenestri/ffenestri.h index 42c74ee63..66c5e2d40 100644 --- a/v2/internal/ffenestri/ffenestri.h +++ b/v2/internal/ffenestri/ffenestri.h @@ -34,4 +34,6 @@ extern void SaveDialog(void *appPointer, char *callbackID, char *title, char *fi extern void DarkModeEnabled(void *appPointer, char *callbackID); extern void UpdateMenu(void *app, char *menuAsJSON); extern void UpdateTray(void *app, char *menuAsJSON); +extern void UpdateContextMenus(void *app, char *contextMenusAsJSON); + #endif diff --git a/v2/internal/ffenestri/ffenestri_client.go b/v2/internal/ffenestri/ffenestri_client.go index a70c9988e..60278be48 100644 --- a/v2/internal/ffenestri/ffenestri_client.go +++ b/v2/internal/ffenestri/ffenestri_client.go @@ -13,6 +13,7 @@ import "C" import ( "encoding/json" + "fmt" "github.com/wailsapp/wails/v2/pkg/menu" "strconv" @@ -188,3 +189,19 @@ func (c *Client) UpdateTray(menu *menu.Menu) { } C.UpdateTray(c.app.app, c.app.string2CString(string(trayMenuJSON))) } + +func (c *Client) UpdateContextMenus(contextMenus *menu.ContextMenus) { + + // Guard against nil contextMenus + if contextMenus == nil { + return + } + // Process the menu + contextMenusJSON, err := json.Marshal(contextMenus) + fmt.Printf("\n\nUPDATED CONTEXT MENUS:\n %+v\n\n", string(contextMenusJSON)) + if err != nil { + c.app.logger.Error("Error processing updated Context Menus: %s", err.Error()) + return + } + C.UpdateContextMenus(c.app.app, c.app.string2CString(string(contextMenusJSON))) +} diff --git a/v2/internal/ffenestri/ffenestri_darwin.c b/v2/internal/ffenestri/ffenestri_darwin.c index 9ed4d90bd..caa417124 100644 --- a/v2/internal/ffenestri/ffenestri_darwin.c +++ b/v2/internal/ffenestri/ffenestri_darwin.c @@ -24,6 +24,7 @@ #define STREQ(a,b) strcmp(a, b) == 0 #define STRCOPY(a) concat(a, "") +#define MEMFREE(input) free((void*)input); input = NULL; #define ON_MAIN_THREAD(str) dispatch( ^{ str; } ) #define MAIN_WINDOW_CALL(str) msg(app->mainWindow, s((str))) @@ -107,6 +108,10 @@ struct hashmap_s menuItemMapForContextMenus; // RadioGroup map for the context menus. Maps a menuitem id with its associated radio group items struct hashmap_s radioGroupMapForContextMenus; +// Context menu data is given by the frontend when clicking a context menu. +// We send this to the backend when an item is selected; +const char *contextMenuData; + // Dispatch Method typedef void (^dispatchMethod)(void); @@ -253,7 +258,7 @@ void Debug(struct Application *app, const char *message, ... ) { va_start(args, message); vsnprintf(logbuffer, MAXMESSAGE, temp, args); app->sendMessageToBackend(&logbuffer[0]); - free((void*)temp); + MEMFREE(temp); va_end(args); } } @@ -264,7 +269,7 @@ void Fatal(struct Application *app, const char *message, ... ) { va_start(args, message); vsnprintf(logbuffer, MAXMESSAGE, temp, args); app->sendMessageToBackend(&logbuffer[0]); - free((void*)temp); + MEMFREE(temp); va_end(args); } @@ -323,21 +328,33 @@ void showContextMenu(struct Application *app, const char *contextMenuID) { return; } + printf("contextMenuID = %s\n", contextMenuID); - ON_MAIN_THREAD ( + // Look for the context menu for this ID + id contextMenu = (id)hashmap_get(&contextMenuMap, (char*)contextMenuID, strlen(contextMenuID)); - // Look for the context menu for this ID - id contextMenu = (id)hashmap_get(&contextMenuMap, (char*)contextMenuID, strlen(contextMenuID)); + printf("CONTEXT MENU = %p\n", contextMenu); - // Grab the content view and show the menu - id contentView = msg(app->mainWindow, s("contentView")); + // Free menu id + MEMFREE(contextMenuID); - // Get the triggering event - id menuEvent = msg(app->mainWindow, s("currentEvent")); + if( contextMenu == NULL ) { + printf("\n\n\n\n\n\n\n"); + dumpHashmap("contextMenuMap", &contextMenuMap); + return; + } + + // Grab the content view and show the menu + id contentView = msg(app->mainWindow, s("contentView")); + printf("contentView = %p\n", contentView); + + // Get the triggering event + id menuEvent = msg(app->mainWindow, s("currentEvent")); + printf("menuEvent = %p\n", menuEvent); + + // Show popup + msg(c("NSMenu"), s("popUpContextMenu:withEvent:forView:"), contextMenu, menuEvent, contentView); - // Show popup - msg(c("NSMenu"), s("popUpContextMenu:withEvent:forView:"), contextMenu, menuEvent, contentView); - ); } void SetColour(struct Application *app, int red, int green, int blue, int alpha) { @@ -353,13 +370,13 @@ void FullSizeContent(struct Application *app) { app->fullSizeContent = 1; } -void Hide(struct Application *app) { +void Hide(struct Application *app) { ON_MAIN_THREAD( msg(app->application, s("hide:")) ); } -void Show(struct Application *app) { +void Show(struct Application *app) { ON_MAIN_THREAD( msg(app->mainWindow, s("makeKeyAndOrderFront:"), NULL); msg(app->application, s("activateIgnoringOtherApps:"), YES); @@ -378,7 +395,9 @@ void messageHandler(id self, SEL cmd, id contentController, id message) { if( strcmp(name, "completed") == 0) { // Delete handler msg(app->manager, s("removeScriptMessageHandlerForName:"), str("completed")); - Show(app); + if (app->startHidden == 0) { + Show(app); + } msg(app->config, s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 0), str("suppressesIncrementalRendering")); } else if( strcmp(name, "windowDrag") == 0 ) { // Guard against null events @@ -397,6 +416,11 @@ void messageHandler(id self, SEL cmd, id contentController, id message) { const char *contextMenuMessage = cstr(msg(message, s("body"))); + if( contextMenuMessage == NULL ) { + printf("EMPTY CONTEXT MENU MESSAGE!!\n"); + return; + } + // Parse the message JsonNode *contextMenuMessageJSON = json_decode(contextMenuMessage); if( contextMenuMessageJSON == NULL ) { @@ -407,42 +431,34 @@ void messageHandler(id self, SEL cmd, id contentController, id message) { // Get menu ID JsonNode *contextMenuIDNode = json_find_member(contextMenuMessageJSON, "id"); if( contextMenuIDNode == NULL ) { - Debug(app, "Error decoding context menu ID: %s", contextMenuMessage); - return; - } - if( contextMenuIDNode->tag != JSON_STRING ) { - Debug(app, "Error decoding context menu ID (Not a string): %s", contextMenuMessage); - return; - } - // TODO: Use the X & Y coordinates of the menu to programmatically open - // the context menu at that point rather than relying on the current NSEvent. - // I got it mostly working (IE not crashing) but the menu was invisible... - // Revisit later + Debug(app, "Error decoding context menu ID: %s", contextMenuMessage); + return; + } + if( contextMenuIDNode->tag != JSON_STRING ) { + Debug(app, "Error decoding context menu ID (Not a string): %s", contextMenuMessage); + return; + } -// // Get menu X -// JsonNode *contextMenuXNode = json_find_member(contextMenuMessageJSON, "x"); -// if( contextMenuXNode == NULL ) { -// Debug(app, "Error decoding context menu X: %s", contextMenuMessage); -// return; -// } -// if( contextMenuXNode->tag != JSON_NUMBER ) { -// Debug(app, "Error decoding context menu X (Not a number): %s", contextMenuMessage); -// return; -// } -// // Get menu Y -// JsonNode *contextMenuYNode = json_find_member(contextMenuMessageJSON, "y"); -// if( contextMenuYNode == NULL ) { -// Debug(app, "Error decoding context menu Y: %s", contextMenuMessage); -// return; -// } -// if( contextMenuYNode->tag != JSON_NUMBER ) { -// Debug(app, "Error decoding context menu Y (Not a number): %s", contextMenuMessage); -// return; -// } + // Get menu Data + JsonNode *contextMenuDataNode = json_find_member(contextMenuMessageJSON, "data"); + if( contextMenuDataNode == NULL ) { + Debug(app, "Error decoding context menu data: %s", contextMenuMessage); + return; + } + if( contextMenuDataNode->tag != JSON_STRING ) { + Debug(app, "Error decoding context menu data (Not a string): %s", contextMenuMessage); + return; + } - ON_MAIN_THREAD( - showContextMenu(app, contextMenuIDNode->string_); - ); + // Save a copy of the context menu data + if ( contextMenuData != NULL ) { + MEMFREE(contextMenuData); + } + contextMenuData = STRCOPY(contextMenuDataNode->string_); + + ON_MAIN_THREAD( + showContextMenu(app, contextMenuIDNode->string_); + ); } else { // const char *m = (const char *)msg(msg(message, s("body")), s("UTF8String")); @@ -451,40 +467,52 @@ void messageHandler(id self, SEL cmd, id contentController, id message) { } } +// Creates a JSON message for the given menuItemID and data +const char* createContextMenuMessage(const char *menuItemID, const char *contextMenuData) { + JsonNode *jsonObject = json_mkobject(); + json_append_member(jsonObject, "menuItemID", json_mkstring(menuItemID)); + json_append_member(jsonObject, "data", json_mkstring(contextMenuData)); + const char *result = json_encode(jsonObject); + json_delete(jsonObject); + return result; +} + // Callback for menu items void menuItemPressedForApplicationMenu(id self, SEL cmd, id sender) { - const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); + const char *menuItemID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); // Notify the backend - const char *message = concat("MC", menuID); + const char *message = concat("MC", menuItemID); messageFromWindowCallback(message); - free((void*)message); + MEMFREE(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")); + const char *menuItemID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); // Notify the backend - const char *message = concat("TC", menuID); + const char *message = concat("TC", menuItemID); messageFromWindowCallback(message); - free((void*)message); + MEMFREE(message); } // Callback for context menu items void menuItemPressedForContextMenus(id self, SEL cmd, id sender) { - const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); + const char *menuItemID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); // Notify the backend - const char *message = concat("XC", menuID); + const char *contextMenuMessage = createContextMenuMessage(menuItemID, contextMenuData); + const char *message = concat("XC", contextMenuMessage); messageFromWindowCallback(message); - free((void*)message); + MEMFREE(message); + MEMFREE(contextMenuMessage); } // Callback for menu items void checkboxMenuItemPressedForApplicationMenu(id self, SEL cmd, id sender, struct hashmap_s *menuItemMap) { - const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); + const char *menuItemID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); // Get the menu item from the menu item map - id menuItem = (id)hashmap_get(&menuItemMapForApplicationMenu, (char*)menuID, strlen(menuID)); + id menuItem = (id)hashmap_get(&menuItemMapForApplicationMenu, (char*)menuItemID, strlen(menuItemID)); // Get the current state bool state = msg(menuItem, s("state")); @@ -493,17 +521,17 @@ void checkboxMenuItemPressedForApplicationMenu(id self, SEL cmd, id sender, stru msg(menuItem, s("setState:"), (state? NSControlStateValueOff : NSControlStateValueOn)); // Notify the backend - const char *message = concat("MC", menuID); + const char *message = concat("MC", menuItemID); messageFromWindowCallback(message); - free((void*)message); + MEMFREE(message); } // 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")); + const char *menuItemID = (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)); + id menuItem = (id)hashmap_get(&menuItemMapForTrayMenu, (char*)menuItemID, strlen(menuItemID)); // Get the current state bool state = msg(menuItem, s("state")); @@ -512,17 +540,17 @@ void checkboxMenuItemPressedForTrayMenu(id self, SEL cmd, id sender, struct hash msg(menuItem, s("setState:"), (state? NSControlStateValueOff : NSControlStateValueOn)); // Notify the backend - const char *message = concat("TC", menuID); + const char *message = concat("TC", menuItemID); messageFromWindowCallback(message); - free((void*)message); + MEMFREE(message); } // Callback for context menu items void checkboxMenuItemPressedForContextMenus(id self, SEL cmd, id sender, struct hashmap_s *menuItemMap) { - const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); + const char *menuItemID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); // Get the menu item from the menu item map - id menuItem = (id)hashmap_get(&menuItemMapForContextMenus, (char*)menuID, strlen(menuID)); + id menuItem = (id)hashmap_get(&menuItemMapForContextMenus, (char*)menuItemID, strlen(menuItemID)); // Get the current state bool state = msg(menuItem, s("state")); @@ -531,17 +559,19 @@ void checkboxMenuItemPressedForContextMenus(id self, SEL cmd, id sender, struct msg(menuItem, s("setState:"), (state? NSControlStateValueOff : NSControlStateValueOn)); // Notify the backend - const char *message = concat("XC", menuID); + const char *contextMenuMessage = createContextMenuMessage(menuItemID, contextMenuData); + const char *message = concat("XC", contextMenuMessage); messageFromWindowCallback(message); - free((void*)message); + MEMFREE(message); + MEMFREE(contextMenuMessage); } // radioMenuItemPressedForApplicationMenu void radioMenuItemPressedForApplicationMenu(id self, SEL cmd, id sender) { - const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); + const char *menuItemID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); // Get the menu item from the menu item map - id menuItem = (id)hashmap_get(&menuItemMapForApplicationMenu, (char*)menuID, strlen(menuID)); + id menuItem = (id)hashmap_get(&menuItemMapForApplicationMenu, (char*)menuItemID, strlen(menuItemID)); // Check the menu items' current state bool selected = msg(menuItem, s("state")); @@ -552,7 +582,7 @@ void radioMenuItemPressedForApplicationMenu(id self, SEL cmd, id sender) { } // Get this item's radio group members and turn them off - id *members = (id*)hashmap_get(&radioGroupMapForApplicationMenu, (char*)menuID, strlen(menuID)); + id *members = (id*)hashmap_get(&radioGroupMapForApplicationMenu, (char*)menuItemID, strlen(menuItemID)); // Uncheck all members of the group id thisMember = members[0]; @@ -567,18 +597,18 @@ void radioMenuItemPressedForApplicationMenu(id self, SEL cmd, id sender) { msg(menuItem, s("setState:"), NSControlStateValueOn); // Notify the backend - const char *message = concat("MC", menuID); + const char *message = concat("MC", menuItemID); messageFromWindowCallback(message); - free((void*)message); + MEMFREE(message); } // radioMenuItemPressedForTrayMenu void radioMenuItemPressedForTrayMenu(id self, SEL cmd, id sender) { - const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); + const char *menuItemID = (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)); + id menuItem = (id)hashmap_get(&menuItemMapForTrayMenu, (char*)menuItemID, strlen(menuItemID)); // Check the menu items' current state bool selected = msg(menuItem, s("state")); @@ -589,7 +619,7 @@ void radioMenuItemPressedForTrayMenu(id self, SEL cmd, id sender) { } // Get this item's radio group members and turn them off - id *members = (id*)hashmap_get(&radioGroupMapForTrayMenu, (char*)menuID, strlen(menuID)); + id *members = (id*)hashmap_get(&radioGroupMapForTrayMenu, (char*)menuItemID, strlen(menuItemID)); // Uncheck all members of the group id thisMember = members[0]; @@ -604,17 +634,17 @@ void radioMenuItemPressedForTrayMenu(id self, SEL cmd, id sender) { msg(menuItem, s("setState:"), NSControlStateValueOn); // Notify the backend - const char *message = concat("TC", menuID); + const char *message = concat("TC", menuItemID); messageFromWindowCallback(message); - free((void*)message); + MEMFREE(message); } // radioMenuItemPressedForContextMenus void radioMenuItemPressedForContextMenus(id self, SEL cmd, id sender) { - const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); + const char *menuItemID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue")); // Get the menu item from the menu item map - id menuItem = (id)hashmap_get(&menuItemMapForContextMenus, (char*)menuID, strlen(menuID)); + id menuItem = (id)hashmap_get(&menuItemMapForContextMenus, (char*)menuItemID, strlen(menuItemID)); // Check the menu items' current state bool selected = msg(menuItem, s("state")); @@ -625,7 +655,7 @@ void radioMenuItemPressedForContextMenus(id self, SEL cmd, id sender) { } // Get this item's radio group members and turn them off - id *members = (id*)hashmap_get(&radioGroupMapForContextMenus, (char*)menuID, strlen(menuID)); + id *members = (id*)hashmap_get(&radioGroupMapForContextMenus, (char*)menuItemID, strlen(menuItemID)); // Uncheck all members of the group id thisMember = members[0]; @@ -639,10 +669,12 @@ void radioMenuItemPressedForContextMenus(id self, SEL cmd, id sender) { // check the selected menu item msg(menuItem, s("setState:"), NSControlStateValueOn); - // Notify the backend - const char *message = concat("XC", menuID); - messageFromWindowCallback(message); - free((void*)message); + // Notify the backend + const char *contextMenuMessage = createContextMenuMessage(menuItemID, contextMenuData); + const char *message = concat("XC", contextMenuMessage); + messageFromWindowCallback(message); + MEMFREE(message); + MEMFREE(contextMenuMessage); } // closeWindow is called when the close button is pressed @@ -651,6 +683,11 @@ void closeWindow(id self, SEL cmd, id sender) { app->sendMessageToBackend("WC"); } +void willFinishLaunching(id self, SEL cmd, id sender) { + struct Application *app = (struct Application *) objc_getAssociatedObject(self, "application"); + printf("\n\n\n\n\n\n\n\n\n\n\n\nI AM HERE!!!!!!!\n\n\n\n\n\n\n\n\n\n\n"); +} + bool isDarkMode(struct Application *app) { id userDefaults = msg(c("NSUserDefaults"), s("standardUserDefaults")); const char *mode = cstr(msg(userDefaults, s("stringForKey:"), str("AppleInterfaceStyle"))); @@ -780,11 +817,12 @@ void* NewApplication(const char *title, int width, int height, int resizable, in // Tray result->trayMenuAsJSON = NULL; - result->processedTrayMenu = NULL; - result->statusItem = NULL; + result->processedTrayMenu = NULL; + result->statusItem = NULL; - // Context Menus - result->contextMenusAsJSON = NULL; + // Context Menus + result->contextMenusAsJSON = NULL; + contextMenuData = NULL; // Window Appearance result->vibrancyLayer = NULL; @@ -818,8 +856,7 @@ void destroyMenu(struct Application *app) { // Release the menu json if we have it if ( app->menuAsJSON != NULL ) { - free((void*)app->menuAsJSON); - app->menuAsJSON = NULL; + MEMFREE(app->menuAsJSON); } // Release processed menu @@ -840,21 +877,27 @@ void destroyContextMenus(struct Application *app) { hashmap_destroy(&menuItemMapForContextMenus); // Free radio group members - if( hashmap_num_entries(&radioGroupMapForContextMenus) > 0 ) { - if (0!=hashmap_iterate_pairs(&radioGroupMapForContextMenus, freeHashmapItem, NULL)) { - Fatal(app, "failed to deallocate hashmap entries!"); - } - } + if( hashmap_num_entries(&radioGroupMapForContextMenus) > 0 ) { + if (0!=hashmap_iterate_pairs(&radioGroupMapForContextMenus, freeHashmapItem, NULL)) { + Fatal(app, "failed to deallocate hashmap entries!"); + } + } - //Free radio groups hashmap - hashmap_destroy(&radioGroupMapForContextMenus); + //Free radio groups hashmap + hashmap_destroy(&radioGroupMapForContextMenus); - //Free context menu map - hashmap_destroy(&contextMenuMap); + //Free context menu map + hashmap_destroy(&contextMenuMap); + + // Destroy processed Context Menus + if( app->processedContextMenus != NULL) { + json_delete(app->processedContextMenus); + app->processedContextMenus = NULL; + } + + // Release the menu json + MEMFREE(app->contextMenusAsJSON); - // Destroy context menu JSON - free((void*)app->contextMenusAsJSON); - app->contextMenusAsJSON = NULL; } @@ -878,12 +921,8 @@ void destroyTray(struct Application *app) { //Free radio groups hashmap hashmap_destroy(&radioGroupMapForTrayMenu); - // Free up the context menu map - hashmap_destroy(&contextMenuMap); - // Release the menu json - free((void*)app->trayMenuAsJSON); - app->trayMenuAsJSON = NULL; + MEMFREE(app->trayMenuAsJSON); // Release processed tray @@ -893,16 +932,12 @@ void destroyTray(struct Application *app) { } } - - - void DestroyApplication(struct Application *app) { Debug(app, "Destroying Application"); // Free the bindings if (app->bindings != NULL) { - free((void*)app->bindings); - app->bindings = NULL; + MEMFREE(app->bindings); } else { Debug(app, "Almost a double free for app->bindings"); } @@ -924,6 +959,11 @@ void DestroyApplication(struct Application *app) { // Destroy the context menus destroyContextMenus(app); + // Clear context menu data if we have it + if( contextMenuData != NULL ) { + MEMFREE(contextMenuData); + } + // Remove script handlers msg(app->manager, s("removeScriptMessageHandlerForName:"), str("contextMenu")); msg(app->manager, s("removeScriptMessageHandlerForName:"), str("windowDrag")); @@ -995,13 +1035,13 @@ void ToggleMaximise(struct Application *app) { ); } -void Maximise(struct Application *app) { +void Maximise(struct Application *app) { if( app->maximised == 0) { ToggleMaximise(app); } } -void Unmaximise(struct Application *app) { +void Unmaximise(struct Application *app) { if( app->maximised == 1) { ToggleMaximise(app); } @@ -1035,7 +1075,7 @@ void dumpFrame(struct Application *app, const char *message, CGRect frame) { Debug(app, "size.height %f", frame.size.height); } -void SetSize(struct Application *app, int width, int height) { +void SetSize(struct Application *app, int width, int height) { ON_MAIN_THREAD( id screen = getCurrentScreen(app); @@ -1051,7 +1091,7 @@ void SetSize(struct Application *app, int width, int height) { ); } -void SetPosition(struct Application *app, int x, int y) { +void SetPosition(struct Application *app, int x, int y) { ON_MAIN_THREAD( id screen = getCurrentScreen(app); CGRect screenFrame = GET_FRAME(screen); @@ -1142,9 +1182,9 @@ void OpenDialog(struct Application *app, char *callbackID, char *title, char *fi app->sendMessageToBackend(responseMessage); // Free memory - free((void*)header); - free((void*)callback); - free((void*)responseMessage); + MEMFREE(header); + MEMFREE(callback); + MEMFREE(responseMessage); }); msg( c("NSApp"), s("runModalForWindow:"), app->mainWindow); @@ -1211,9 +1251,9 @@ void SaveDialog(struct Application *app, char *callbackID, char *title, char *fi app->sendMessageToBackend(responseMessage); // Free memory - free((void*)header); - free((void*)callback); - free((void*)responseMessage); + MEMFREE(header); + MEMFREE(callback); + MEMFREE(responseMessage); }); msg( c("NSApp"), s("runModalForWindow:"), app->mainWindow); @@ -1289,7 +1329,7 @@ void SetContextMenus(struct Application *app, const char *contextMenusAsJSON) { void SetBindings(struct Application *app, const char *bindings) { const char* temp = concat("window.wailsbindings = \"", bindings); const char* jscall = concat(temp, "\";"); - free((void*)temp); + MEMFREE(temp); app->bindings = jscall; } @@ -1360,9 +1400,9 @@ void DarkModeEnabled(struct Application *app, const char *callbackID) { app->sendMessageToBackend(responseMessage); // Free memory - free((void*)header); - free((void*)callback); - free((void*)responseMessage); + MEMFREE(header); + MEMFREE(callback); + MEMFREE(responseMessage); ); } @@ -1372,14 +1412,15 @@ void createDelegate(struct Application *app) { bool resultAddProtoc = class_addProtocol(delegateClass, objc_getProtocol("NSApplicationDelegate")); class_addMethod(delegateClass, s("applicationShouldTerminateAfterLastWindowClosed:"), (IMP) yes, "c@:@"); class_addMethod(delegateClass, s("applicationWillTerminate:"), (IMP) closeWindow, "v@:@"); + class_addMethod(delegateClass, s("applicationWillFinishLaunching:"), (IMP) willFinishLaunching, "v@:@"); // Menu Callbacks 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@:@"); + class_addMethod(delegateClass, s("checkboxMenuCallbackForTrayMenu:"), (IMP) checkboxMenuItemPressedForTrayMenu, "v@:@"); + class_addMethod(delegateClass, s("radioMenuCallbackForTrayMenu:"), (IMP) radioMenuItemPressedForTrayMenu, "v@:@"); class_addMethod(delegateClass, s("menuCallbackForContextMenus:"), (IMP)menuItemPressedForContextMenus, "v@:@"); class_addMethod(delegateClass, s("checkboxMenuCallbackForContextMenus:"), (IMP) checkboxMenuItemPressedForContextMenus, "v@:@"); class_addMethod(delegateClass, s("radioMenuCallbackForContextMenus:"), (IMP) radioMenuItemPressedForContextMenus, "v@:@"); @@ -1600,7 +1641,7 @@ const char* getJSONString(JsonNode *item, const char* key) { const char *result = ""; if ( node != NULL && node->tag == JSON_STRING) { result = node->string_; - } + } return result; } @@ -1609,7 +1650,7 @@ bool getJSONBool(JsonNode *item, const char* key, bool *result) { if ( node != NULL && node->tag == JSON_BOOL) { *result = node->bool_; return true; - } + } return false; } @@ -1618,7 +1659,7 @@ bool getJSONInt(JsonNode *item, const char* key, int *result) { if ( node != NULL && node->tag == JSON_NUMBER) { *result = (int) node->number_; return true; - } + } return false; } @@ -1832,7 +1873,7 @@ id parseTextMenuItem(struct Application *app, id parentMenu, const char *title, 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")); @@ -1939,12 +1980,12 @@ struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char if ( label == NULL) { label = "(empty)"; } - + const char *menuid = getJSONString(item, "ID"); if ( menuid == NULL) { menuid = ""; } - + bool disabled = false; getJSONBool(item, "Disabled", &disabled); @@ -2150,34 +2191,56 @@ void UpdateMenu(struct Application *app, const char *menuAsJSON) { ); } +void dumpContextMenus(struct Application *app) { + dumpHashmap("menuItemMapForContextMenus", &menuItemMapForContextMenus); + printf("&menuItemMapForContextMenus = %p\n", &menuItemMapForContextMenus); + + //Free radio groups hashmap + dumpHashmap("radioGroupMapForContextMenus", &radioGroupMapForContextMenus); + printf("&radioGroupMapForContextMenus = %p\n", &radioGroupMapForContextMenus); + + //Free context menu map + dumpHashmap("contextMenuMap", &contextMenuMap); + printf("&contextMenuMap = %p\n", &contextMenuMap); +} + void parseContextMenus(struct Application *app) { // Allocation the hashmaps we need allocateContextMenuHashMaps(app); // Parse the context menu json - app->processedContextMenus = json_decode(app->contextMenusAsJSON); + app->processedContextMenus = json_decode(app->contextMenusAsJSON); - if( app->processedContextMenus == NULL ) { - // Parse error! - Fatal(app, "Unable to parse Context Menus JSON: %s", app->contextMenusAsJSON); - return; - } - - // Iterate context menus - JsonNode *contextMenu; - json_foreach(contextMenu, app->processedContextMenus) { - // Create a new menu - id menu = createMenu(str("")); - - // parse the menu - parseMenu(app, menu, contextMenu, &menuItemMapForContextMenus, - "checkboxMenuCallbackForContextMenus:", "radioMenuCallbackForContextMenus:", "menuCallbackForContextMenus:"); - - // Store the item in the context menu map - hashmap_put(&contextMenuMap, (char*)contextMenu->key, strlen(contextMenu->key), menu); + if( app->processedContextMenus == NULL ) { + // Parse error! + Fatal(app, "Unable to parse Context Menus JSON: %s", app->contextMenusAsJSON); + return; } + JsonNode *contextMenuItems = json_find_member(app->processedContextMenus, "Items"); + if( contextMenuItems == NULL ) { + // Parse error! + Fatal(app, "Unable to find Items:", app->processedContextMenus); + return; + } + // Iterate context menus + JsonNode *contextMenu; + json_foreach(contextMenu, contextMenuItems) { + // Create a new menu + id menu = createMenu(str("")); + printf("Context menu NSMenu pointer = %p\n", menu); + + // parse the menu + parseMenu(app, menu, contextMenu, &menuItemMapForContextMenus, + "checkboxMenuCallbackForContextMenus:", "radioMenuCallbackForContextMenus:", "menuCallbackForContextMenus:"); + + // Store the item in the context menu map + printf("Putting context menu %p with key '%s' in contextMenuMap %p\n", menu, contextMenu->key, &contextMenuMap); + hashmap_put(&contextMenuMap, (char*)contextMenu->key, strlen(contextMenu->key), menu); + } + + dumpContextMenus(app); } void parseTrayData(struct Application *app) { @@ -2186,26 +2249,26 @@ void parseTrayData(struct Application *app) { allocateTrayHashMaps(app); // Create a new menu - id traymenu = createMenu(str("")); + id traymenu = createMenu(str("")); id statusItem = app->statusItem; // Create a new menu bar if we need to if ( statusItem == NULL ) { - id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") ); - statusItem = msg(statusBar, s("statusItemWithLength:"), -1.0); - app->statusItem = statusItem; - msg(statusItem, s("retain")); - id statusBarButton = msg(statusItem, s("button")); + id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") ); + statusItem = msg(statusBar, s("statusItemWithLength:"), -1.0); + app->statusItem = statusItem; + msg(statusItem, s("retain")); + id statusBarButton = msg(statusItem, s("button")); - // If we have a tray icon - if ( trayIconLength > 0 ) { - id imageData = msg(c("NSData"), s("dataWithBytes:length:"), trayIcon, trayIconLength); - id trayImage = ALLOC("NSImage"); - msg(trayImage, s("initWithData:"), imageData); - msg(statusBarButton, s("setImage:"), trayImage); - } - } + // If we have a tray icon + if ( trayIconLength > 0 ) { + id imageData = msg(c("NSData"), s("dataWithBytes:length:"), trayIcon, trayIconLength); + id trayImage = ALLOC("NSImage"); + msg(trayImage, s("initWithData:"), imageData); + msg(statusBarButton, s("setImage:"), trayImage); + } + } // Parse the processed menu json app->processedTrayMenu = json_decode(app->trayMenuAsJSON); @@ -2250,7 +2313,7 @@ void parseTrayData(struct Application *app) { // msg(c("NSString"), s("stringWithUTF8String:"), tray->icon))); - msg(statusItem, s("setMenu:"), traymenu); + msg(statusItem, s("setMenu:"), traymenu); } @@ -2266,6 +2329,20 @@ void UpdateTray(struct Application *app, const char *trayMenuAsJSON) { ); } +void UpdateContextMenus(struct Application *app, const char *contextMenusAsJSON) { + ON_MAIN_THREAD ( + + dumpContextMenus(app); + + // Free up memory + destroyContextMenus(app); + + // Set the context menu JSON + app->contextMenusAsJSON = contextMenusAsJSON; + parseContextMenus(app); + ); +} + void Run(struct Application *app, int argc, char **argv) { @@ -2378,11 +2455,11 @@ void Run(struct Application *app, int argc, char **argv) { // We want to evaluate the internal code plus runtime before the assets const char *temp = concat(invoke, app->bindings); const char *internalCode = concat(temp, (const char*)&runtime); - free((void*)temp); + MEMFREE(temp); // Add code that sets up the initial state, EG: State Stores. temp = concat(internalCode, getInitialState(app)); - free((void*)internalCode); + MEMFREE(internalCode); internalCode = temp; // Loop over assets and build up one giant Mother Of All Evals @@ -2397,7 +2474,7 @@ void Run(struct Application *app, int argc, char **argv) { } temp = concat(internalCode, (const char *)asset); - free((void*)internalCode); + MEMFREE(internalCode); internalCode = temp; index++; }; @@ -2405,14 +2482,14 @@ void Run(struct Application *app, int argc, char **argv) { // Disable context menu if not in debug mode if( debug != 1 ) { temp = concat(internalCode, "wails._.DisableDefaultContextMenu();"); - free((void*)internalCode); + MEMFREE(internalCode); internalCode = temp; } // class_addMethod(delegateClass, s("applicationWillFinishLaunching:"), (IMP) willFinishLaunching, "@@:@"); // Include callback after evaluation temp = concat(internalCode, "webkit.messageHandlers.completed.postMessage(true);"); - free((void*)internalCode); + MEMFREE(internalCode); internalCode = temp; // const char *viewportScriptString = "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); meta.setAttribute('initial-scale', '1.0'); meta.setAttribute('maximum-scale', '1.0'); meta.setAttribute('minimum-scale', '1.0'); meta.setAttribute('user-scalable', 'no'); document.getElementsByTagName('head')[0].appendChild(meta);"; @@ -2449,11 +2526,14 @@ void Run(struct Application *app, int argc, char **argv) { parseContextMenus(app); } + // We set it to be invisible by default. It will become visible when everything has initialised + msg(app->mainWindow, s("setIsVisible:"), NO); + // Finally call run Debug(app, "Run called"); msg(app->application, s("run")); - free((void*)internalCode); + MEMFREE(internalCode); } #endif diff --git a/v2/internal/ffenestri/ffenestri_darwin.go b/v2/internal/ffenestri/ffenestri_darwin.go index 92061184f..9a2c24da4 100644 --- a/v2/internal/ffenestri/ffenestri_darwin.go +++ b/v2/internal/ffenestri/ffenestri_darwin.go @@ -21,6 +21,7 @@ extern void SetContextMenus(void *, const char *); import "C" import ( "encoding/json" + "fmt" "github.com/wailsapp/wails/v2/pkg/options" ) @@ -116,6 +117,7 @@ func (a *Application) processPlatformSettings() error { contextMenus := options.GetContextMenus(a.config) if contextMenus != nil { contextMenusJSON, err := json.Marshal(contextMenus) + fmt.Printf("\n\nCONTEXT MENUS:\n %+v\n\n", string(contextMenusJSON)) if err != nil { return err } diff --git a/v2/internal/messagedispatcher/dispatchclient.go b/v2/internal/messagedispatcher/dispatchclient.go index ff5d94cba..f5602b674 100644 --- a/v2/internal/messagedispatcher/dispatchclient.go +++ b/v2/internal/messagedispatcher/dispatchclient.go @@ -33,6 +33,7 @@ type Client interface { DarkModeEnabled(callbackID string) UpdateMenu(menu *menu.Menu) UpdateTray(menu *menu.Menu) + UpdateContextMenus(contextMenus *menu.ContextMenus) } // DispatchClient is what the frontends use to interface with the diff --git a/v2/internal/messagedispatcher/message/contextmenus.go b/v2/internal/messagedispatcher/message/contextmenus.go new file mode 100644 index 000000000..53acebd0e --- /dev/null +++ b/v2/internal/messagedispatcher/message/contextmenus.go @@ -0,0 +1,43 @@ +package message + +import ( + "fmt" + + "github.com/wailsapp/wails/v2/pkg/menu" +) + +// ContextMenusOnMessage is used to emit listener registration requests +// on the service bus +type ContextMenusOnMessage 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, string) +} + +// contextMenusMessageParser does what it says on the tin! +func contextMenusMessageParser(message string) (*parsedMessage, error) { + + // Sanity check: Menu messages must be at least 2 bytes + if len(message) < 3 { + return nil, fmt.Errorf("context menus message was an invalid length") + } + + var topic string + var data interface{} + + // Switch the message type + switch message[1] { + case 'C': + contextMenuData := message[2:] + topic = "contextmenus:clicked" + data = contextMenuData + 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/messagedispatcher/message/messageparser.go b/v2/internal/messagedispatcher/message/messageparser.go index 2666cdbc8..50671d3e8 100644 --- a/v2/internal/messagedispatcher/message/messageparser.go +++ b/v2/internal/messagedispatcher/message/messageparser.go @@ -20,6 +20,7 @@ var messageParsers = map[byte]func(string) (*parsedMessage, error){ 'S': systemMessageParser, 'M': menuMessageParser, 'T': trayMessageParser, + 'X': contextMenusMessageParser, } // Parse will attempt to parse the given message diff --git a/v2/internal/messagedispatcher/message/tray.go b/v2/internal/messagedispatcher/message/tray.go index 76739e2cc..e59205aab 100644 --- a/v2/internal/messagedispatcher/message/tray.go +++ b/v2/internal/messagedispatcher/message/tray.go @@ -20,7 +20,7 @@ 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") + return nil, fmt.Errorf("tray message was an invalid length") } var topic string diff --git a/v2/internal/messagedispatcher/messagedispatcher.go b/v2/internal/messagedispatcher/messagedispatcher.go index 6f6a977a7..cad34d55e 100644 --- a/v2/internal/messagedispatcher/messagedispatcher.go +++ b/v2/internal/messagedispatcher/messagedispatcher.go @@ -17,15 +17,16 @@ import ( // Dispatcher translates messages received from the frontend // and publishes them onto the service bus type Dispatcher struct { - quitChannel <-chan *servicebus.Message - resultChannel <-chan *servicebus.Message - eventChannel <-chan *servicebus.Message - windowChannel <-chan *servicebus.Message - dialogChannel <-chan *servicebus.Message - systemChannel <-chan *servicebus.Message - menuChannel <-chan *servicebus.Message - trayChannel <-chan *servicebus.Message - running bool + quitChannel <-chan *servicebus.Message + resultChannel <-chan *servicebus.Message + eventChannel <-chan *servicebus.Message + windowChannel <-chan *servicebus.Message + dialogChannel <-chan *servicebus.Message + systemChannel <-chan *servicebus.Message + menuChannel <-chan *servicebus.Message + contextMenuChannel <-chan *servicebus.Message + trayChannel <-chan *servicebus.Message + running bool servicebus *servicebus.ServiceBus logger logger.CustomLogger @@ -77,23 +78,29 @@ func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher, return nil, err } + contextMenuChannel, err := servicebus.Subscribe("contextmenufrontend:") + if err != nil { + return nil, err + } + trayChannel, err := servicebus.Subscribe("trayfrontend:") if err != nil { return nil, err } result := &Dispatcher{ - servicebus: servicebus, - eventChannel: eventChannel, - logger: logger.CustomLogger("Message Dispatcher"), - clients: make(map[string]*DispatchClient), - resultChannel: resultChannel, - quitChannel: quitChannel, - windowChannel: windowChannel, - dialogChannel: dialogChannel, - systemChannel: systemChannel, - menuChannel: menuChannel, - trayChannel: trayChannel, + servicebus: servicebus, + eventChannel: eventChannel, + logger: logger.CustomLogger("Message Dispatcher"), + clients: make(map[string]*DispatchClient), + resultChannel: resultChannel, + quitChannel: quitChannel, + windowChannel: windowChannel, + dialogChannel: dialogChannel, + systemChannel: systemChannel, + menuChannel: menuChannel, + trayChannel: trayChannel, + contextMenuChannel: contextMenuChannel, } return result, nil @@ -125,6 +132,8 @@ func (d *Dispatcher) Start() error { d.processSystemMessage(systemMessage) case menuMessage := <-d.menuChannel: d.processMenuMessage(menuMessage) + case contextMenuMessage := <-d.contextMenuChannel: + d.processContextMenuMessage(contextMenuMessage) case trayMessage := <-d.trayChannel: d.processTrayMessage(trayMessage) } @@ -446,6 +455,34 @@ func (d *Dispatcher) processMenuMessage(result *servicebus.Message) { d.logger.Error("Unknown menufrontend command: %s", command) } } +func (d *Dispatcher) processContextMenuMessage(result *servicebus.Message) { + splitTopic := strings.Split(result.Topic(), ":") + if len(splitTopic) < 2 { + d.logger.Error("Invalid contextmenu message : %#v", result.Data()) + return + } + + command := splitTopic[1] + switch command { + case "update": + + updatedContextMenus, ok := result.Data().(*menu.ContextMenus) + if !ok { + d.logger.Error("Invalid data for 'contextmenufrontend:update' : %#v", + result.Data()) + return + } + + // TODO: Work out what we mean in a multi window environment... + // For now we will just pick the first one + for _, client := range d.clients { + client.frontend.UpdateContextMenus(updatedContextMenus) + } + + default: + d.logger.Error("Unknown contextmenufrontend command: %s", command) + } +} func (d *Dispatcher) processTrayMessage(result *servicebus.Message) { splitTopic := strings.Split(result.Topic(), ":") diff --git a/v2/internal/runtime/contextmenus.go b/v2/internal/runtime/contextmenus.go new file mode 100644 index 000000000..787752dc9 --- /dev/null +++ b/v2/internal/runtime/contextmenus.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" +) + +// ContextMenus defines all ContextMenu related operations +type ContextMenus interface { + On(menuID string, callback func(*menu.MenuItem, string)) + Update() + GetByID(menuID string) *menu.MenuItem + RemoveByID(id string) bool +} + +type contextMenus struct { + bus *servicebus.ServiceBus + contextmenus *menu.ContextMenus +} + +// newContextMenus creates a new ContextMenu struct +func newContextMenus(bus *servicebus.ServiceBus, contextmenus *menu.ContextMenus) ContextMenus { + return &contextMenus{ + bus: bus, + contextmenus: contextmenus, + } +} + +// On registers a listener for a particular event +func (t *contextMenus) On(menuID string, callback func(*menu.MenuItem, string)) { + t.bus.Publish("contextmenus:on", &message.ContextMenusOnMessage{ + MenuID: menuID, + Callback: callback, + }) +} + +func (t *contextMenus) Update() { + t.bus.Publish("contextmenus:update", t.contextmenus) +} + +func (t *contextMenus) GetByID(menuItemID string) *menu.MenuItem { + return t.contextmenus.GetByID(menuItemID) +} + +func (t *contextMenus) RemoveByID(menuItemID string) bool { + return t.contextmenus.RemoveByID(menuItemID) +} diff --git a/v2/internal/runtime/js/desktop/darwin.js b/v2/internal/runtime/js/desktop/darwin.js index 831463ab5..a38fa0727 100644 --- a/v2/internal/runtime/js/desktop/darwin.js +++ b/v2/internal/runtime/js/desktop/darwin.js @@ -57,10 +57,10 @@ export function Init() { e.preventDefault(); } if( contextMenuId != null ) { + let contextData = currentElement.dataset['wails-context-menu-data']; let message = { id: contextMenuId, - x: e.clientX, - y: e.clientY, + data: contextData || "", }; window.webkit.messageHandlers.contextMenu.postMessage(JSON.stringify(message)); } diff --git a/v2/internal/runtime/runtime.go b/v2/internal/runtime/runtime.go index a652e80cc..30a4d7dde 100644 --- a/v2/internal/runtime/runtime.go +++ b/v2/internal/runtime/runtime.go @@ -7,30 +7,32 @@ import ( // Runtime is a means for the user to interact with the application at runtime type Runtime struct { - Browser Browser - Events Events - Window Window - Dialog Dialog - System System - Menu Menu - Tray Tray - Store *StoreProvider - Log Log - bus *servicebus.ServiceBus + Browser Browser + Events Events + Window Window + Dialog Dialog + System System + Menu Menu + ContextMenu ContextMenus + Tray Tray + Store *StoreProvider + Log Log + bus *servicebus.ServiceBus } // New creates a new runtime -func New(serviceBus *servicebus.ServiceBus, menu *menu.Menu, trayMenu *menu.Menu) *Runtime { +func New(serviceBus *servicebus.ServiceBus, menu *menu.Menu, trayMenu *menu.Menu, contextMenus *menu.ContextMenus) *Runtime { result := &Runtime{ - Browser: newBrowser(), - Events: newEvents(serviceBus), - Window: newWindow(serviceBus), - Dialog: newDialog(serviceBus), - System: newSystem(serviceBus), - Menu: newMenu(serviceBus, menu), - Tray: newTray(serviceBus, trayMenu), - Log: newLog(serviceBus), - bus: serviceBus, + Browser: newBrowser(), + Events: newEvents(serviceBus), + Window: newWindow(serviceBus), + Dialog: newDialog(serviceBus), + System: newSystem(serviceBus), + Menu: newMenu(serviceBus, menu), + Tray: newTray(serviceBus, trayMenu), + ContextMenu: newContextMenus(serviceBus, contextMenus), + Log: newLog(serviceBus), + bus: serviceBus, } result.Store = newStore(result) return result diff --git a/v2/internal/runtime/tray.go b/v2/internal/runtime/tray.go index b3c3ab6f0..1695bb1a0 100644 --- a/v2/internal/runtime/tray.go +++ b/v2/internal/runtime/tray.go @@ -20,7 +20,7 @@ type trayRuntime struct { } // newTray creates a new Menu struct -func newTray(bus *servicebus.ServiceBus, menu *menu.Menu) Menu { +func newTray(bus *servicebus.ServiceBus, menu *menu.Menu) Tray { return &trayRuntime{ bus: bus, trayMenu: menu, diff --git a/v2/internal/subsystem/contextmenus.go b/v2/internal/subsystem/contextmenus.go new file mode 100644 index 000000000..ea058e70e --- /dev/null +++ b/v2/internal/subsystem/contextmenus.go @@ -0,0 +1,200 @@ +package subsystem + +import ( + "encoding/json" + "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" +) + +// ContextMenus is the subsystem that handles the operation of context menus. It manages all service bus messages +// starting with "contextmenus". +type ContextMenus struct { + quitChannel <-chan *servicebus.Message + menuChannel <-chan *servicebus.Message + running bool + + // Event listeners + listeners map[string][]func(*menu.MenuItem, string) + menuItems map[string]*menu.MenuItem + notifyLock sync.RWMutex + + // logger + logger logger.CustomLogger + + // The context menus + contextMenus *menu.ContextMenus + + // Service Bus + bus *servicebus.ServiceBus +} + +// NewContextMenus creates a new context menu subsystem +func NewContextMenus(contextMenus *menu.ContextMenus, bus *servicebus.ServiceBus, logger *logger.Logger) (*ContextMenus, error) { + + // Register quit channel + quitChannel, err := bus.Subscribe("quit") + if err != nil { + return nil, err + } + + // Subscribe to menu messages + menuChannel, err := bus.Subscribe("contextmenus:") + if err != nil { + return nil, err + } + + result := &ContextMenus{ + quitChannel: quitChannel, + menuChannel: menuChannel, + logger: logger.CustomLogger("Context Menu Subsystem"), + listeners: make(map[string][]func(*menu.MenuItem, string)), + menuItems: make(map[string]*menu.MenuItem), + contextMenus: contextMenus, + bus: bus, + } + + // Build up list of item/id pairs + result.processContextMenus(contextMenus) + + return result, nil +} + +type contextMenuData struct { + MenuItemID string `json:"menuItemID"` + Data string `json:"data"` +} + +// Start the subsystem +func (c *ContextMenus) Start() error { + + c.logger.Trace("Starting") + + c.running = true + + // Spin off a go routine + go func() { + for c.running { + select { + case <-c.quitChannel: + c.running = false + break + case menuMessage := <-c.menuChannel: + splitTopic := strings.Split(menuMessage.Topic(), ":") + menuMessageType := splitTopic[1] + switch menuMessageType { + case "clicked": + if len(splitTopic) != 2 { + c.logger.Error("Received clicked message with invalid topic format. Expected 2 sections in topic, got %s", splitTopic) + continue + } + c.logger.Trace("Got Context Menu clicked Message: %s %+v", menuMessage.Topic(), menuMessage.Data()) + contextMenuDataJSON := menuMessage.Data().(string) + + var data contextMenuData + err := json.Unmarshal([]byte(contextMenuDataJSON), &data) + if err != nil { + c.logger.Trace("Cannot process contextMenuDataJSON %s", string(contextMenuDataJSON)) + return + } + + // Get the menu item + menuItem := c.menuItems[data.MenuItemID] + if menuItem == nil { + c.logger.Trace("Cannot process menuitem id %s - unknown", data.MenuItemID) + return + } + + // Is the menu item a checkbox? + if menuItem.Type == menu.CheckboxType { + // Toggle state + menuItem.Checked = !menuItem.Checked + } + + // Notify listeners + c.notifyListeners(data, menuItem) + case "on": + listenerDetails := menuMessage.Data().(*message.ContextMenusOnMessage) + id := listenerDetails.MenuID + c.listeners[id] = append(c.listeners[id], listenerDetails.Callback) + + // Make sure we catch any menu updates + case "update": + updatedMenu := menuMessage.Data().(*menu.ContextMenus) + c.processContextMenus(updatedMenu) + + // Notify frontend of menu change + c.bus.Publish("contextmenufrontend:update", updatedMenu) + + default: + c.logger.Error("unknown context menu message: %+v", menuMessage) + } + } + } + + // Call shutdown + c.shutdown() + }() + + return nil +} + +func (c *ContextMenus) processContextMenus(contextMenus *menu.ContextMenus) { + // Initialise the variables + c.menuItems = make(map[string]*menu.MenuItem) + c.contextMenus = contextMenus + + for _, contextMenu := range contextMenus.Items { + for _, item := range contextMenu.Items { + c.processMenuItem(item) + } + } +} + +func (c *ContextMenus) processMenuItem(item *menu.MenuItem) { + + if item.SubMenu != nil { + for _, submenuitem := range item.SubMenu { + c.processMenuItem(submenuitem) + } + return + } + + if item.ID != "" { + if c.menuItems[item.ID] != nil { + c.logger.Error("Context Menu id '%s' is used by multiple menu items: %s %s", c.menuItems[item.ID].Label, item.Label) + return + } + c.menuItems[item.ID] = item + } +} + +// Notifies listeners that the given menu was clicked +func (c *ContextMenus) notifyListeners(contextData contextMenuData, menuItem *menu.MenuItem) { + + // Get list of menu listeners + listeners := c.listeners[contextData.MenuItemID] + if listeners == nil { + c.logger.Trace("No listeners for MenuItem with ID '%s'", contextData.MenuItemID) + return + } + + // Lock the listeners + c.notifyLock.Lock() + + // Callback in goroutine + for _, listener := range listeners { + go listener(menuItem, contextData.Data) + } + + // Unlock + c.notifyLock.Unlock() +} + +func (c *ContextMenus) shutdown() { + c.logger.Trace("Shutdown") +} diff --git a/v2/internal/subsystem/runtime.go b/v2/internal/subsystem/runtime.go index 98d81a771..c24603faa 100644 --- a/v2/internal/subsystem/runtime.go +++ b/v2/internal/subsystem/runtime.go @@ -24,7 +24,7 @@ type Runtime struct { } // NewRuntime creates a new runtime subsystem -func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, menu *menu.Menu, trayMenu *menu.Menu) (*Runtime, error) { +func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, menu *menu.Menu, trayMenu *menu.Menu, contextMenus *menu.ContextMenus) (*Runtime, error) { // Register quit channel quitChannel, err := bus.Subscribe("quit") @@ -42,7 +42,7 @@ func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, menu *menu.Me quitChannel: quitChannel, runtimeChannel: runtimeChannel, logger: logger.CustomLogger("Runtime Subsystem"), - runtime: runtime.New(bus, menu, trayMenu), + runtime: runtime.New(bus, menu, trayMenu, contextMenus), } return result, nil diff --git a/v2/pkg/menu/contextmenu.go b/v2/pkg/menu/contextmenu.go new file mode 100644 index 000000000..e15bee427 --- /dev/null +++ b/v2/pkg/menu/contextmenu.go @@ -0,0 +1,38 @@ +package menu + +type ContextMenus struct { + Items map[string]*Menu +} + +func NewContextMenus() *ContextMenus { + return &ContextMenus{ + Items: make(map[string]*Menu), + } +} + +func (c *ContextMenus) AddMenu(ID string, menu *Menu) { + c.Items[ID] = menu +} + +func (c *ContextMenus) GetByID(menuID string) *MenuItem { + + // Loop over menu items + for _, item := range c.Items { + result := item.GetByID(menuID) + if result != nil { + return result + } + } + return nil +} + +func (c *ContextMenus) RemoveByID(id string) bool { + // Loop over menu items + for _, item := range c.Items { + result := item.RemoveByID(id) + if result == true { + return result + } + } + return false +} diff --git a/v2/pkg/options/mac/mac.go b/v2/pkg/options/mac/mac.go index db276d6a9..f6c7ea17d 100644 --- a/v2/pkg/options/mac/mac.go +++ b/v2/pkg/options/mac/mac.go @@ -10,5 +10,5 @@ type Options struct { WindowBackgroundIsTranslucent bool Menu *menu.Menu Tray *menu.Menu - ContextMenus map[string]*menu.Menu + ContextMenus *menu.ContextMenus } diff --git a/v2/pkg/options/options.go b/v2/pkg/options/options.go index d6c8187fa..f2a1d6ddf 100644 --- a/v2/pkg/options/options.go +++ b/v2/pkg/options/options.go @@ -24,7 +24,7 @@ type App struct { StartHidden bool DevTools bool RGBA int - ContextMenus map[string]*menu.Menu + ContextMenus *menu.ContextMenus Tray *menu.Menu Menu *menu.Menu Mac *mac.Options @@ -97,11 +97,11 @@ func GetApplicationMenu(appoptions *App) *menu.Menu { return result } -func GetContextMenus(appoptions *App) map[string]*menu.Menu { - var result map[string]*menu.Menu +func GetContextMenus(appoptions *App) *menu.ContextMenus { + var result *menu.ContextMenus result = appoptions.ContextMenus - var contextMenuOverrides map[string]*menu.Menu + var contextMenuOverrides *menu.ContextMenus switch runtime.GOOS { case "darwin": if appoptions.Mac != nil { @@ -118,8 +118,10 @@ func GetContextMenus(appoptions *App) map[string]*menu.Menu { } // Overwrite defaults with OS Specific context menus - for id, contextMenu := range contextMenuOverrides { - result[id] = contextMenu + if contextMenuOverrides != nil { + for id, contextMenu := range contextMenuOverrides.Items { + result.AddMenu(id, contextMenu) + } } return result diff --git a/v2/test/kitchensink/contextmenus.go b/v2/test/kitchensink/contextmenus.go new file mode 100644 index 000000000..008554e44 --- /dev/null +++ b/v2/test/kitchensink/contextmenus.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "sync" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/menu" +) + +// ContextMenu struct +type ContextMenu struct { + runtime *wails.Runtime + counter int + lock sync.Mutex +} + +// WailsInit is called at application startup +func (c *ContextMenu) WailsInit(runtime *wails.Runtime) error { + // Perform your setup here + c.runtime = runtime + + // Setup Menu Listeners + c.runtime.ContextMenu.On("Test Context Menu", func(mi *menu.MenuItem, contextData string) { + fmt.Printf("\n\nContext Data = '%s'\n\n", contextData) + c.lock.Lock() + c.counter++ + mi.Label = fmt.Sprintf("Clicked %d times", c.counter) + c.lock.Unlock() + c.runtime.ContextMenu.Update() + }) + + return nil +} + +func createContextMenus() *menu.ContextMenus { + result := menu.NewContextMenus() + result.AddMenu("test", menu.NewMenuFromItems(menu.Text("Clicked 0 times", "Test Context Menu"))) + return result +} diff --git a/v2/test/kitchensink/frontend/src/App.svelte b/v2/test/kitchensink/frontend/src/App.svelte index a1edade6e..bb178d7c2 100644 --- a/v2/test/kitchensink/frontend/src/App.svelte +++ b/v2/test/kitchensink/frontend/src/App.svelte @@ -43,7 +43,7 @@
-