5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-03 00:41:59 +08:00

Support context menu data

Support StartHidden
This commit is contained in:
Lea Anthony 2020-12-18 15:50:25 +11:00
parent 34ac62e4ac
commit a8995c5377
No known key found for this signature in database
GPG Key ID: 33DAF7BB90A58405
23 changed files with 773 additions and 247 deletions

View File

@ -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{}) {
}

View File

@ -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 {

View File

@ -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

View File

@ -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)))
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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(), ":")

View File

@ -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)
}

View File

@ -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));
}

View File

@ -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

View File

@ -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,

View File

@ -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")
}

View File

@ -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

View File

@ -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
}

View File

@ -10,5 +10,5 @@ type Options struct {
WindowBackgroundIsTranslucent bool
Menu *menu.Menu
Tray *menu.Menu
ContextMenus map[string]*menu.Menu
ContextMenus *menu.ContextMenus
}

View File

@ -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

View File

@ -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
}

View File

@ -43,7 +43,7 @@
<!-- Sticky alerts (toasts), empty container -->
<div class="sticky-alerts"></div>
<!-- Sidebar -->
<div class="sidebar noselect" data-wails-context-menu-id="test">
<div class="sidebar noselect" data-wails-context-menu-id="test" data-wails-context-menu-data="hello!">
<div data-wails-no-drag class="sidebar-menu">
<!-- Sidebar brand -->
<div on:click="{ homepageClicked }" class="sidebar-brand">

View File

@ -3,7 +3,6 @@ package main
import (
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/mac"
"log"
@ -20,9 +19,8 @@ func main() {
MinHeight: 600,
//Tray: menu.NewMenuFromItems(menu.AppMenu()),
//Menu: menu.NewMenuFromItems(menu.AppMenu()),
ContextMenus: map[string]*menu.Menu{
"test": menu.NewMenuFromItems(menu.Text("Test Menu", "Test Context Menu")),
},
StartHidden: true,
ContextMenus: createContextMenus(),
Mac: &mac.Options{
WebviewIsTransparent: true,
WindowBackgroundIsTranslucent: true,
@ -46,6 +44,7 @@ func main() {
app.Bind(&Window{})
app.Bind(&Menu{})
app.Bind(&Tray{})
app.Bind(&ContextMenu{})
err = app.Run()
if err != nil {