diff --git a/v2/internal/app/desktop.go b/v2/internal/app/desktop.go index 58987b69..50d6fe5d 100644 --- a/v2/internal/app/desktop.go +++ b/v2/internal/app/desktop.go @@ -78,11 +78,11 @@ func CreateApp(options *options.App) *App { func (a *App) Run() error { // Setup signal handler - signal, err := signal.NewManager(a.servicebus, a.logger) + signalsubsystem, err := signal.NewManager(a.servicebus, a.logger) if err != nil { return err } - a.signal = signal + a.signal = signalsubsystem a.signal.Start() // Start the service bus @@ -90,11 +90,12 @@ func (a *App) Run() error { a.servicebus.Start() // Start the runtime - runtime, err := subsystem.NewRuntime(a.servicebus, a.logger) + runtimesubsystem, err := subsystem.NewRuntime(a.servicebus, a.logger, + a.options.Mac.Menu) if err != nil { return err } - a.runtime = runtime + a.runtime = runtimesubsystem a.runtime.Start() // Application Stores @@ -102,11 +103,12 @@ func (a *App) Run() error { a.appconfigStore = a.runtime.GoRuntime().Store.New("wails:appconfig", a.options) // Start the binding subsystem - binding, err := subsystem.NewBinding(a.servicebus, a.logger, a.bindings, a.runtime.GoRuntime()) + bindingsubsystem, err := subsystem.NewBinding(a.servicebus, a.logger, + a.bindings, a.runtime.GoRuntime()) if err != nil { return err } - a.binding = binding + a.binding = bindingsubsystem a.binding.Start() // Start the logging subsystem @@ -145,11 +147,11 @@ func (a *App) Run() error { default: return fmt.Errorf("unsupported OS: %s", goruntime.GOOS) } - menu, err := subsystem.NewMenu(platformMenu, a.servicebus, a.logger) + menusubsystem, err := subsystem.NewMenu(platformMenu, a.servicebus, a.logger) if err != nil { return err } - a.menu = menu + a.menu = menusubsystem a.menu.Start() // Start the call subsystem diff --git a/v2/internal/ffenestri/ffenestri.h b/v2/internal/ffenestri/ffenestri.h index ab097859..4c914fc4 100644 --- a/v2/internal/ffenestri/ffenestri.h +++ b/v2/internal/ffenestri/ffenestri.h @@ -32,4 +32,5 @@ extern void DisableFrame(void *app); extern void OpenDialog(void *appPointer, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int allowFiles, int allowDirs, int allowMultiple, int showHiddenFiles, int canCreateDirectories, int resolvesAliases, int treatPackagesAsDirectories); extern void SaveDialog(void *appPointer, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories); extern void DarkModeEnabled(void *appPointer, char *callbackID); +extern void UpdateMenu(void *app, char *menuAsJSON); #endif diff --git a/v2/internal/ffenestri/ffenestri_client.go b/v2/internal/ffenestri/ffenestri_client.go index d9ca073f..d5627d4e 100644 --- a/v2/internal/ffenestri/ffenestri_client.go +++ b/v2/internal/ffenestri/ffenestri_client.go @@ -12,6 +12,8 @@ package ffenestri import "C" import ( + "encoding/json" + "github.com/wailsapp/wails/v2/pkg/menu" "strconv" "github.com/wailsapp/wails/v2/internal/logger" @@ -154,3 +156,19 @@ func (c *Client) SaveDialog(dialogOptions *options.SaveDialog, callbackID string func (c *Client) DarkModeEnabled(callbackID string) { C.DarkModeEnabled(c.app.app, c.app.string2CString(callbackID)) } + +func (c *Client) UpdateMenu(menu *menu.Menu) { + + // Guard against nil menus + if menu == nil { + return + } + // Process the menu + processedMenu := NewProcessedMenu(menu) + menuJSON, err := json.Marshal(processedMenu) + if err != nil { + c.app.logger.Error("Error processing updated Menu: %s", err.Error()) + return + } + C.UpdateMenu(c.app.app, c.app.string2CString(string(menuJSON))) +} diff --git a/v2/internal/ffenestri/ffenestri_darwin.c b/v2/internal/ffenestri/ffenestri_darwin.c index 414f088b..4221b047 100644 --- a/v2/internal/ffenestri/ffenestri_darwin.c +++ b/v2/internal/ffenestri/ffenestri_darwin.c @@ -85,34 +85,34 @@ typedef void (^dispatchMethod)(void); // dispatch will execute the given `func` pointer void dispatch(dispatchMethod func) { - dispatch_async(dispatch_get_main_queue(), func); + dispatch_async(dispatch_get_main_queue(), func); } // App Delegate typedef struct AppDel { - Class isa; - id window; + Class isa; + id window; } AppDelegate; // Credit: https://stackoverflow.com/a/8465083 char* concat(const char *string1, const char *string2) { - const size_t len1 = strlen(string1); - const size_t len2 = strlen(string2); - char *result = malloc(len1 + len2 + 1); - memcpy(result, string1, len1); - memcpy(result + len1, string2, len2 + 1); - return result; + const size_t len1 = strlen(string1); + const size_t len2 = strlen(string2); + char *result = malloc(len1 + len2 + 1); + memcpy(result, string1, len1); + memcpy(result + len1, string2, len2 + 1); + return result; } // yes command simply returns YES! BOOL yes(id self, SEL cmd) { - return YES; + return YES; } // Prints a hashmap entry -int hashmap_log(void const *context, struct hashmap_element_s const *e) { +int hashmap_log(void *const context, struct hashmap_element_s *const e) { printf("%s: %p ", (char*)e->key, e->data); return 0; } @@ -121,7 +121,7 @@ int hashmap_log(void const *context, struct hashmap_element_s const *e) { void dumpHashmap(const char *name, struct hashmap_s *hashmap) { printf("%s = { ", name); if (0!=hashmap_iterate_pairs(hashmap, hashmap_log, NULL)) { - fprintf(stderr, "Failed to dump hashmap entries\n"); + fprintf(stderr, "Failed to dump hashmap entries\n"); } printf("}\n"); } @@ -130,78 +130,78 @@ extern void messageFromWindowCallback(const char *); typedef void (*ffenestriCallback)(const char *); void HideMouse() { - msg(c("NSCursor"), s("hide")); + msg(c("NSCursor"), s("hide")); } void ShowMouse() { - msg(c("NSCursor"), s("unhide")); + msg(c("NSCursor"), s("unhide")); } struct Application { - // Cocoa data - id application; - id delegate; - id mainWindow; - id wkwebview; - id manager; - id config; - id mouseEvent; - id mouseDownMonitor; - id mouseUpMonitor; - id vibrancyLayer; + // Cocoa data + id application; + id delegate; + id mainWindow; + id wkwebview; + id manager; + id config; + id mouseEvent; + id mouseDownMonitor; + id mouseUpMonitor; + id vibrancyLayer; - // Window Data - const char *title; - int width; - int height; - int minWidth; - int minHeight; - int maxWidth; - int maxHeight; - int resizable; - int devtools; - int fullscreen; - int red; - int green; - int blue; - int alpha; - int webviewIsTranparent; - const char *appearance; - int decorations; - bool dragging; - int logLevel; + // Window Data + const char *title; + int width; + int height; + int minWidth; + int minHeight; + int maxWidth; + int maxHeight; + int resizable; + int devtools; + int fullscreen; + int red; + int green; + int blue; + int alpha; + int webviewIsTranparent; + const char *appearance; + int decorations; + bool dragging; + int logLevel; - // Features - int frame; - int startHidden; - int maximised; - int minimised; - int titlebarAppearsTransparent; - int hideTitle; - int hideTitleBar; - int fullSizeContent; - int useToolBar; - int hideToolbarSeparator; - int windowBackgroundIsTranslucent; - - // Menu - const char *menuAsJSON; - id menubar; - JsonNode *processedMenu; + // Features + int frame; + int startHidden; + int maximised; + int minimised; + int titlebarAppearsTransparent; + int hideTitle; + int hideTitleBar; + int fullSizeContent; + int useToolBar; + int hideToolbarSeparator; + int windowBackgroundIsTranslucent; - // User Data - char *HTML; + // Menu + const char *menuAsJSON; + id menubar; + JsonNode *processedMenu; - // Callback - ffenestriCallback sendMessageToBackend; + // User Data + char *HTML; - // Bindings - const char *bindings; + // Callback + ffenestriCallback sendMessageToBackend; - // Lock - used for sync operations (Should we be using g_mutex?) - int lock; + // Bindings + const char *bindings; + + // Lock - used for sync operations (Should we be using g_mutex?) + int lock; }; @@ -209,18 +209,18 @@ struct Application { // Credit: https://stackoverflow.com/a/20639708 // 5k is more than enough for a log message -#define MAXMESSAGE 1024*5 +#define MAXMESSAGE 1024*10 char logbuffer[MAXMESSAGE]; void Debug(struct Application *app, const char *message, ... ) { - if ( debug ) { - const char *temp = concat("LTFfenestri (C) | ", message); - va_list args; - va_start(args, message); - vsnprintf(logbuffer, MAXMESSAGE, temp, args); - app->sendMessageToBackend(&logbuffer[0]); - free((void*)temp); - va_end(args); - } + if ( debug ) { + const char *temp = concat("LTFfenestri (C) | ", message); + va_list args; + va_start(args, message); + vsnprintf(logbuffer, MAXMESSAGE, temp, args); + app->sendMessageToBackend(&logbuffer[0]); + free((void*)temp); + va_end(args); + } } void Fatal(struct Application *app, const char *message, ... ) { @@ -234,106 +234,106 @@ void Fatal(struct Application *app, const char *message, ... ) { } void TitlebarAppearsTransparent(struct Application* app) { - app->titlebarAppearsTransparent = 1; + app->titlebarAppearsTransparent = 1; } void HideTitle(struct Application *app) { - app->hideTitle = 1; + app->hideTitle = 1; } void HideTitleBar(struct Application *app) { - app->hideTitleBar = 1; + app->hideTitleBar = 1; } void HideToolbarSeparator(struct Application *app) { - app->hideToolbarSeparator = 1; + app->hideToolbarSeparator = 1; } void UseToolbar(struct Application *app) { - app->useToolBar = 1; + app->useToolBar = 1; } // WebviewIsTransparent will make the webview transparent // revealing the Cocoa window underneath void WebviewIsTransparent(struct Application *app) { - app->webviewIsTranparent = 1; + app->webviewIsTranparent = 1; } // SetAppearance will set the window's Appearance to the // given value void SetAppearance(struct Application *app, const char *appearance) { - app->appearance = appearance; + app->appearance = appearance; } void applyWindowColour(struct Application *app) { - // Apply the colour only if the window has been created - if( app->mainWindow != NULL ) { - ON_MAIN_THREAD( - id colour = msg(c("NSColor"), s("colorWithCalibratedRed:green:blue:alpha:"), - (float)app->red / 255.0, - (float)app->green / 255.0, - (float)app->blue / 255.0, - (float)app->alpha / 255.0); - msg(app->mainWindow, s("setBackgroundColor:"), colour); - ); - } + // Apply the colour only if the window has been created + if( app->mainWindow != NULL ) { + ON_MAIN_THREAD( + id colour = msg(c("NSColor"), s("colorWithCalibratedRed:green:blue:alpha:"), + (float)app->red / 255.0, + (float)app->green / 255.0, + (float)app->blue / 255.0, + (float)app->alpha / 255.0); + msg(app->mainWindow, s("setBackgroundColor:"), colour); + ); + } } void SetColour(struct Application *app, int red, int green, int blue, int alpha) { - app->red = red; - app->green = green; - app->blue = blue; - app->alpha = alpha; + app->red = red; + app->green = green; + app->blue = blue; + app->alpha = alpha; - applyWindowColour(app); + applyWindowColour(app); } void FullSizeContent(struct Application *app) { - app->fullSizeContent = 1; + app->fullSizeContent = 1; } void Hide(struct Application *app) { - ON_MAIN_THREAD( - msg(app->application, s("hide:")) - ); + ON_MAIN_THREAD( + msg(app->application, s("hide:")) + ); } void Show(struct Application *app) { - ON_MAIN_THREAD( - msg(app->mainWindow, s("makeKeyAndOrderFront:"), NULL); - msg(app->application, s("activateIgnoringOtherApps:"), YES); - ); + ON_MAIN_THREAD( + msg(app->mainWindow, s("makeKeyAndOrderFront:"), NULL); + msg(app->application, s("activateIgnoringOtherApps:"), YES); + ); } void SetWindowBackgroundIsTranslucent(struct Application *app) { - app->windowBackgroundIsTranslucent = 1; + app->windowBackgroundIsTranslucent = 1; } // Sends messages to the backend void messageHandler(id self, SEL cmd, id contentController, id message) { - struct Application *app = (struct Application *)objc_getAssociatedObject( - self, "application"); - const char *name = (const char *)msg(msg(message, s("name")), s("UTF8String")); - if( strcmp(name, "completed") == 0) { - // Delete handler - msg(app->manager, s("removeScriptMessageHandlerForName:"), str("completed")); - 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 - if( app->mouseEvent != NULL ) { - HideMouse(); - app->dragging = true; - ON_MAIN_THREAD( - msg(app->mainWindow, s("performWindowDragWithEvent:"), app->mouseEvent); - ); - } - } else { - // const char *m = (const char *)msg(msg(message, s("body")), s("UTF8String")); - const char *m = cstr(msg(message, s("body"))); - app->sendMessageToBackend(m); - } + struct Application *app = (struct Application *)objc_getAssociatedObject( + self, "application"); + const char *name = (const char *)msg(msg(message, s("name")), s("UTF8String")); + if( strcmp(name, "completed") == 0) { + // Delete handler + msg(app->manager, s("removeScriptMessageHandlerForName:"), str("completed")); + 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 + if( app->mouseEvent != NULL ) { + HideMouse(); + app->dragging = true; + ON_MAIN_THREAD( + msg(app->mainWindow, s("performWindowDragWithEvent:"), app->mouseEvent); + ); + } + } else { + // const char *m = (const char *)msg(msg(message, s("body")), s("UTF8String")); + const char *m = cstr(msg(message, s("body"))); + app->sendMessageToBackend(m); + } } // Callback for menu items @@ -376,7 +376,7 @@ void radioMenuItemPressed(id self, SEL cmd, id sender) { // If it's already selected, exit early if (selected) { - return; + return; } // Get this item's radio group members and turn them off @@ -386,9 +386,9 @@ void radioMenuItemPressed(id self, SEL cmd, id sender) { id thisMember = members[0]; int count = 0; while(thisMember != NULL) { - msg(thisMember, s("setState:"), NSControlStateValueOff); - count = count + 1; - thisMember = members[count]; + msg(thisMember, s("setState:"), NSControlStateValueOff); + count = count + 1; + thisMember = members[count]; } // check the selected menu item @@ -402,34 +402,34 @@ void radioMenuItemPressed(id self, SEL cmd, id sender) { // closeWindow is called when the close button is pressed void closeWindow(id self, SEL cmd, id sender) { - struct Application *app = (struct Application *) objc_getAssociatedObject(self, "application"); - app->sendMessageToBackend("WC"); + struct Application *app = (struct Application *) objc_getAssociatedObject(self, "application"); + app->sendMessageToBackend("WC"); } bool isDarkMode(struct Application *app) { - id userDefaults = msg(c("NSUserDefaults"), s("standardUserDefaults")); - const char *mode = cstr(msg(userDefaults, s("stringForKey:"), str("AppleInterfaceStyle"))); - return ( mode != NULL && strcmp(mode, "Dark") == 0 ); + id userDefaults = msg(c("NSUserDefaults"), s("standardUserDefaults")); + const char *mode = cstr(msg(userDefaults, s("stringForKey:"), str("AppleInterfaceStyle"))); + return ( mode != NULL && strcmp(mode, "Dark") == 0 ); } void ExecJS(struct Application *app, const char *js) { - ON_MAIN_THREAD( - msg(app->wkwebview, - s("evaluateJavaScript:completionHandler:"), - str(js), - NULL); - ); + ON_MAIN_THREAD( + msg(app->wkwebview, + s("evaluateJavaScript:completionHandler:"), + str(js), + NULL); + ); } void themeChanged(id self, SEL cmd, id sender) { - struct Application *app = (struct Application *)objc_getAssociatedObject( - self, "application"); - bool currentThemeIsDark = isDarkMode(app); - if ( currentThemeIsDark ) { - ExecJS(app, "window.wails.Events.Emit( 'wails:system:themechange', true );"); - } else { - ExecJS(app, "window.wails.Events.Emit( 'wails:system:themechange', false );"); - } + struct Application *app = (struct Application *)objc_getAssociatedObject( + self, "application"); + bool currentThemeIsDark = isDarkMode(app); + if ( currentThemeIsDark ) { + ExecJS(app, "window.wails.Events.Emit( 'wails:system:themechange', true );"); + } else { + ExecJS(app, "window.wails.Events.Emit( 'wails:system:themechange', false );"); + } } // void willFinishLaunching(id self) { @@ -437,413 +437,423 @@ void themeChanged(id self, SEL cmd, id sender) { // Debug(app, "willFinishLaunching called!"); // } -void* NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel) { - // Setup main application struct - struct Application *result = malloc(sizeof(struct Application)); - result->title = title; - result->width = width; - result->height = height; - result->minWidth = 0; - result->minHeight = 0; - result->maxWidth = 0; - result->maxHeight = 0; - result->resizable = resizable; - result->devtools = devtools; - result->fullscreen = fullscreen; - result->lock = 0; - result->maximised = 0; - result->minimised = 0; - result->startHidden = startHidden; - result->decorations = 0; - result->logLevel = logLevel; +void allocateHashMaps(struct Application *app) { + // Allocate new menuItem map + if( 0 != hashmap_create((const unsigned)16, &menuItemMap)) { + // Couldn't allocate map + Fatal(app, "Not enough memory to allocate menuItemMap!"); + return; + } - result->mainWindow = NULL; - result->mouseEvent = NULL; - result->mouseDownMonitor = NULL; - result->mouseUpMonitor = NULL; - - result->dragging = false; - - // Features - result->frame = 1; - result->hideTitle = 0; - result->hideTitleBar = 0; - result->fullSizeContent = 0; - result->useToolBar = 0; - result->hideToolbarSeparator = 0; - result->appearance = NULL; - result->windowBackgroundIsTranslucent = 0; - - // Window data - result->vibrancyLayer = NULL; - result->delegate = NULL; - - // Menu - result->menuAsJSON = NULL; - result->processedMenu = NULL; - - result->titlebarAppearsTransparent = 0; - result->webviewIsTranparent = 0; - - result->sendMessageToBackend = (ffenestriCallback) messageFromWindowCallback; - - - // Allocate new menuItem map - if( 0 != hashmap_create((const unsigned)16, &menuItemMap)) { - // Couldn't allocate map - Fatal(result, "Not enough memory to allocate menuItemMap!"); - return NULL; - } - - // Allocate the Radio Group Cache - if( 0 != hashmap_create((const unsigned)4, &radioGroupMap)) { - // Couldn't allocate map - Fatal(result, "Not enough memory to allocate radioGroupMap!"); - return NULL; - } - - return (void*) result; + // Allocate the Radio Group Cache + if( 0 != hashmap_create((const unsigned)4, &radioGroupMap)) { + // Couldn't allocate map + Fatal(app, "Not enough memory to allocate radioGroupMap!"); + return; + } } -int freeHashmapItem(void *context, struct hashmap_element_s const *e) { +void* NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel) { + // Setup main application struct + struct Application *result = malloc(sizeof(struct Application)); + result->title = title; + result->width = width; + result->height = height; + result->minWidth = 0; + result->minHeight = 0; + result->maxWidth = 0; + result->maxHeight = 0; + result->resizable = resizable; + result->devtools = devtools; + result->fullscreen = fullscreen; + result->lock = 0; + result->maximised = 0; + result->minimised = 0; + result->startHidden = startHidden; + result->decorations = 0; + result->logLevel = logLevel; + + result->mainWindow = NULL; + result->mouseEvent = NULL; + result->mouseDownMonitor = NULL; + result->mouseUpMonitor = NULL; + + result->dragging = false; + + // Features + result->frame = 1; + result->hideTitle = 0; + result->hideTitleBar = 0; + result->fullSizeContent = 0; + result->useToolBar = 0; + result->hideToolbarSeparator = 0; + result->appearance = NULL; + result->windowBackgroundIsTranslucent = 0; + + // Window data + result->vibrancyLayer = NULL; + result->delegate = NULL; + + // Menu + result->menuAsJSON = NULL; + result->processedMenu = NULL; + + result->titlebarAppearsTransparent = 0; + result->webviewIsTranparent = 0; + + result->sendMessageToBackend = (ffenestriCallback) messageFromWindowCallback; + + return (void*) result; +} + +int freeHashmapItem(void *const context, struct hashmap_element_s *const e) { free(e->data); return -1; } +void destroyMenu(struct Application *app) { + + // Free menu item hashmap + hashmap_destroy(&menuItemMap); + + // Free radio group members + if( hashmap_num_entries(&radioGroupMap) > 0 ) { + if (0!=hashmap_iterate_pairs(&radioGroupMap, freeHashmapItem, NULL)) { + Fatal(app, "failed to deallocate hashmap entries!"); + } + } + + //Free radio groups hashmap + hashmap_destroy(&radioGroupMap); + + // Release the menu json if we have it + if ( app->menuAsJSON != NULL ) { + free((void*)app->menuAsJSON); + app->menuAsJSON = NULL; + } + + // Release processed menu + if( app->processedMenu != NULL) { + json_delete(app->processedMenu); + app->processedMenu = NULL; + } +} + void DestroyApplication(struct Application *app) { - Debug(app, "Destroying Application"); + Debug(app, "Destroying Application"); - // Free the bindings - if (app->bindings != NULL) { - free((void*)app->bindings); - app->bindings = NULL; - } else { - Debug(app, "Almost a double free for app->bindings"); - } + // Free the bindings + if (app->bindings != NULL) { + free((void*)app->bindings); + app->bindings = NULL; + } else { + Debug(app, "Almost a double free for app->bindings"); + } - // Remove mouse monitors - if( app->mouseDownMonitor != NULL ) { - msg( c("NSEvent"), s("removeMonitor:"), app->mouseDownMonitor); - } - if( app->mouseUpMonitor != NULL ) { - msg( c("NSEvent"), s("removeMonitor:"), app->mouseUpMonitor); - } + // Remove mouse monitors + if( app->mouseDownMonitor != NULL ) { + msg( c("NSEvent"), s("removeMonitor:"), app->mouseDownMonitor); + } + if( app->mouseUpMonitor != NULL ) { + msg( c("NSEvent"), s("removeMonitor:"), app->mouseUpMonitor); + } - // Free menu item hashmap - hashmap_destroy(&menuItemMap); + destroyMenu(app); - // Free radio group members - if (0!=hashmap_iterate_pairs(&radioGroupMap, freeHashmapItem, NULL)) { - Fatal(app, "failed to deallocate hashmap entries!"); - } + // Remove script handlers + msg(app->manager, s("removeScriptMessageHandlerForName:"), str("windowDrag")); + msg(app->manager, s("removeScriptMessageHandlerForName:"), str("external")); - //Free radio groups hashmap - hashmap_destroy(&radioGroupMap); + // Close main window + msg(app->mainWindow, s("close")); - // Release the menu json if we have it - if ( app->menuAsJSON != NULL ) { - free(app->menuAsJSON); - } - - // Release processed menu - if( app->processedMenu != NULL) { - json_delete(app->processedMenu); - } - - // Remove script handlers - msg(app->manager, s("removeScriptMessageHandlerForName:"), str("windowDrag")); - msg(app->manager, s("removeScriptMessageHandlerForName:"), str("external")); - - // Close main window - msg(app->mainWindow, s("close")); - - // Terminate app - msg(c("NSApp"), s("terminate:"), NULL); - Debug(app, "Finished Destroying Application"); + // Terminate app + msg(c("NSApp"), s("terminate:"), NULL); + Debug(app, "Finished Destroying Application"); } // Quit will stop the cocoa application and free up all the memory // used by the application void Quit(struct Application *app) { - Debug(app, "Quit Called"); - DestroyApplication(app); + Debug(app, "Quit Called"); + DestroyApplication(app); } // SetTitle sets the main window title to the given string void SetTitle(struct Application *app, const char *title) { - Debug(app, "SetTitle Called"); - ON_MAIN_THREAD( - msg(app->mainWindow, s("setTitle:"), str(title)); - ); + Debug(app, "SetTitle Called"); + ON_MAIN_THREAD( + msg(app->mainWindow, s("setTitle:"), str(title)); + ); } void ToggleFullscreen(struct Application *app) { - ON_MAIN_THREAD( - app->fullscreen = !app->fullscreen; - MAIN_WINDOW_CALL("toggleFullScreen:"); - ); + ON_MAIN_THREAD( + app->fullscreen = !app->fullscreen; + MAIN_WINDOW_CALL("toggleFullScreen:"); + ); } bool isFullScreen(struct Application *app) { - int mask = (int)msg(app->mainWindow, s("styleMask")); - bool result = (mask & NSWindowStyleMaskFullscreen) == NSWindowStyleMaskFullscreen; - return result; + int mask = (int)msg(app->mainWindow, s("styleMask")); + bool result = (mask & NSWindowStyleMaskFullscreen) == NSWindowStyleMaskFullscreen; + return result; } // Fullscreen sets the main window to be fullscreen void Fullscreen(struct Application *app) { - Debug(app, "Fullscreen Called"); - if( ! isFullScreen(app) ) { - ToggleFullscreen(app); - } + Debug(app, "Fullscreen Called"); + if( ! isFullScreen(app) ) { + ToggleFullscreen(app); + } } // UnFullscreen resets the main window after a fullscreen void UnFullscreen(struct Application *app) { - Debug(app, "UnFullscreen Called"); - if( isFullScreen(app) ) { - ToggleFullscreen(app); - } + Debug(app, "UnFullscreen Called"); + if( isFullScreen(app) ) { + ToggleFullscreen(app); + } } void Center(struct Application *app) { - Debug(app, "Center Called"); - ON_MAIN_THREAD( - MAIN_WINDOW_CALL("center"); - ); + Debug(app, "Center Called"); + ON_MAIN_THREAD( + MAIN_WINDOW_CALL("center"); + ); } void ToggleMaximise(struct Application *app) { - ON_MAIN_THREAD( - app->maximised = !app->maximised; - MAIN_WINDOW_CALL("zoom:"); - ); + ON_MAIN_THREAD( + app->maximised = !app->maximised; + MAIN_WINDOW_CALL("zoom:"); + ); } void Maximise(struct Application *app) { - if( app->maximised == 0) { - ToggleMaximise(app); - } + if( app->maximised == 0) { + ToggleMaximise(app); + } } void Unmaximise(struct Application *app) { - if( app->maximised == 1) { - ToggleMaximise(app); - } + if( app->maximised == 1) { + ToggleMaximise(app); + } } void Minimise(struct Application *app) { - ON_MAIN_THREAD( - MAIN_WINDOW_CALL("miniaturize:"); - ); + ON_MAIN_THREAD( + MAIN_WINDOW_CALL("miniaturize:"); + ); } void Unminimise(struct Application *app) { - ON_MAIN_THREAD( - MAIN_WINDOW_CALL("deminiaturize:"); - ); + ON_MAIN_THREAD( + MAIN_WINDOW_CALL("deminiaturize:"); + ); } id getCurrentScreen(struct Application *app) { - id screen = NULL; - screen = msg(app->mainWindow, s("screen")); - if( screen == NULL ) { - screen = msg(c("NSScreen"), u("mainScreen")); - } - return screen; + id screen = NULL; + screen = msg(app->mainWindow, s("screen")); + if( screen == NULL ) { + screen = msg(c("NSScreen"), u("mainScreen")); + } + return screen; } void dumpFrame(struct Application *app, const char *message, CGRect frame) { - Debug(app, message); - Debug(app, "origin.x %f", frame.origin.x); - Debug(app, "origin.y %f", frame.origin.y); - Debug(app, "size.width %f", frame.size.width); - Debug(app, "size.height %f", frame.size.height); + Debug(app, message); + Debug(app, "origin.x %f", frame.origin.x); + Debug(app, "origin.y %f", frame.origin.y); + Debug(app, "size.width %f", frame.size.width); + Debug(app, "size.height %f", frame.size.height); } void SetSize(struct Application *app, int width, int height) { - ON_MAIN_THREAD( - id screen = getCurrentScreen(app); + ON_MAIN_THREAD( + id screen = getCurrentScreen(app); - // Get the rect for the window - CGRect frame = GET_FRAME(app->mainWindow); + // Get the rect for the window + CGRect frame = GET_FRAME(app->mainWindow); - // Credit: https://github.com/patr0nus/DeskGap/blob/73c0ac9f2c73f55b6e81f64f6673a7962b5719cd/lib/src/platform/mac/util/NSScreen%2BGeometry.m - frame.origin.y = (frame.origin.y + frame.size.height) - (float)height; - frame.size.width = (float)width; - frame.size.height = (float)height; + // Credit: https://github.com/patr0nus/DeskGap/blob/73c0ac9f2c73f55b6e81f64f6673a7962b5719cd/lib/src/platform/mac/util/NSScreen%2BGeometry.m + frame.origin.y = (frame.origin.y + frame.size.height) - (float)height; + frame.size.width = (float)width; + frame.size.height = (float)height; - msg(app->mainWindow, s("setFrame:display:animate:"), frame, 1, 0); - ); + msg(app->mainWindow, s("setFrame:display:animate:"), frame, 1, 0); + ); } void SetPosition(struct Application *app, int x, int y) { - ON_MAIN_THREAD( - id screen = getCurrentScreen(app); - CGRect screenFrame = GET_FRAME(screen); - CGRect windowFrame = GET_FRAME(app->mainWindow); + ON_MAIN_THREAD( + id screen = getCurrentScreen(app); + CGRect screenFrame = GET_FRAME(screen); + CGRect windowFrame = GET_FRAME(app->mainWindow); - windowFrame.origin.x = screenFrame.origin.x + (float)x; - windowFrame.origin.y = (screenFrame.origin.y + screenFrame.size.height) - windowFrame.size.height - (float)y; - msg(app->mainWindow, s("setFrame:display:animate:"), windowFrame, 1, 0); - ); + windowFrame.origin.x = screenFrame.origin.x + (float)x; + windowFrame.origin.y = (screenFrame.origin.y + screenFrame.size.height) - windowFrame.size.height - (float)y; + msg(app->mainWindow, s("setFrame:display:animate:"), windowFrame, 1, 0); + ); } // OpenDialog opens a dialog to select files/directories void OpenDialog(struct Application *app, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int allowFiles, int allowDirs, int allowMultiple, int showHiddenFiles, int canCreateDirectories, int resolvesAliases, int treatPackagesAsDirectories) { - Debug(app, "OpenDialog Called with callback id: %s", callbackID); + Debug(app, "OpenDialog Called with callback id: %s", callbackID); - // Create an open panel - ON_MAIN_THREAD( + // Create an open panel + ON_MAIN_THREAD( - // Create the dialog - id dialog = msg(c("NSOpenPanel"), s("openPanel")); + // Create the dialog + id dialog = msg(c("NSOpenPanel"), s("openPanel")); - // Valid but appears to do nothing.... :/ - msg(dialog, s("setTitle:"), str(title)); + // Valid but appears to do nothing.... :/ + msg(dialog, s("setTitle:"), str(title)); - // Filters - if( filters != NULL && strlen(filters) > 0) { - id filterString = msg(str(filters), s("stringByReplacingOccurrencesOfString:withString:"), str("*."), str("")); - filterString = msg(filterString, s("stringByReplacingOccurrencesOfString:withString:"), str(" "), str("")); - id filterList = msg(filterString, s("componentsSeparatedByString:"), str(",")); - msg(dialog, s("setAllowedFileTypes:"), filterList); - } else { - msg(dialog, s("setAllowsOtherFileTypes:"), YES); - } + // Filters + if( filters != NULL && strlen(filters) > 0) { + id filterString = msg(str(filters), s("stringByReplacingOccurrencesOfString:withString:"), str("*."), str("")); + filterString = msg(filterString, s("stringByReplacingOccurrencesOfString:withString:"), str(" "), str("")); + id filterList = msg(filterString, s("componentsSeparatedByString:"), str(",")); + msg(dialog, s("setAllowedFileTypes:"), filterList); + } else { + msg(dialog, s("setAllowsOtherFileTypes:"), YES); + } - // Default Directory - if( defaultDir != NULL && strlen(defaultDir) > 0 ) { - msg(dialog, s("setDirectoryURL:"), url(defaultDir)); - } + // Default Directory + if( defaultDir != NULL && strlen(defaultDir) > 0 ) { + msg(dialog, s("setDirectoryURL:"), url(defaultDir)); + } - // Default Filename - if( defaultFilename != NULL && strlen(defaultFilename) > 0 ) { - msg(dialog, s("setNameFieldStringValue:"), str(defaultFilename)); - } + // Default Filename + if( defaultFilename != NULL && strlen(defaultFilename) > 0 ) { + msg(dialog, s("setNameFieldStringValue:"), str(defaultFilename)); + } - // Setup Options - msg(dialog, s("setCanChooseFiles:"), allowFiles); - msg(dialog, s("setCanChooseDirectories:"), allowDirs); - msg(dialog, s("setAllowsMultipleSelection:"), allowMultiple); - msg(dialog, s("setShowsHiddenFiles:"), showHiddenFiles); - msg(dialog, s("setCanCreateDirectories:"), canCreateDirectories); - msg(dialog, s("setResolvesAliases:"), resolvesAliases); - msg(dialog, s("setTreatsFilePackagesAsDirectories:"), treatPackagesAsDirectories); + // Setup Options + msg(dialog, s("setCanChooseFiles:"), allowFiles); + msg(dialog, s("setCanChooseDirectories:"), allowDirs); + msg(dialog, s("setAllowsMultipleSelection:"), allowMultiple); + msg(dialog, s("setShowsHiddenFiles:"), showHiddenFiles); + msg(dialog, s("setCanCreateDirectories:"), canCreateDirectories); + msg(dialog, s("setResolvesAliases:"), resolvesAliases); + msg(dialog, s("setTreatsFilePackagesAsDirectories:"), treatPackagesAsDirectories); - // Setup callback handler - msg(dialog, s("beginSheetModalForWindow:completionHandler:"), app->mainWindow, ^(id result) { - - // Create the response JSON object - JsonNode *response = json_mkarray(); + // Setup callback handler + msg(dialog, s("beginSheetModalForWindow:completionHandler:"), app->mainWindow, ^(id result) { - // If the user selected some files - if( result == (id)1 ) { - // Grab the URLs returned - id urls = msg(dialog, s("URLs")); + // Create the response JSON object + JsonNode *response = json_mkarray(); - // Iterate over all the selected files - int noOfResults = (int)msg(urls, s("count")); - for( int index = 0; index < noOfResults; index++ ) { + // If the user selected some files + if( result == (id)1 ) { + // Grab the URLs returned + id urls = msg(dialog, s("URLs")); - // Extract the filename - id url = msg(urls, s("objectAtIndex:"), index); - const char *filename = (const char *)msg(msg(url, s("path")), s("UTF8String")); + // Iterate over all the selected files + int noOfResults = (int)msg(urls, s("count")); + for( int index = 0; index < noOfResults; index++ ) { - // Add the the response array - json_append_element(response, json_mkstring(filename)); - } - } + // Extract the filename + id url = msg(urls, s("objectAtIndex:"), index); + const char *filename = (const char *)msg(msg(url, s("path")), s("UTF8String")); - // Create JSON string and free json memory - char *encoded = json_stringify(response, ""); - json_delete(response); + // Add the the response array + json_append_element(response, json_mkstring(filename)); + } + } - // Construct callback message. Format "D|" - const char *callback = concat("DO", callbackID); - const char *header = concat(callback, "|"); - const char *responseMessage = concat(header, encoded); + // Create JSON string and free json memory + char *encoded = json_stringify(response, ""); + json_delete(response); - // Send message to backend - app->sendMessageToBackend(responseMessage); + // Construct callback message. Format "D|" + const char *callback = concat("DO", callbackID); + const char *header = concat(callback, "|"); + const char *responseMessage = concat(header, encoded); - // Free memory - free((void*)header); - free((void*)callback); - free((void*)responseMessage); - }); + // Send message to backend + app->sendMessageToBackend(responseMessage); - msg( c("NSApp"), s("runModalForWindow:"), app->mainWindow); - ); + // Free memory + free((void*)header); + free((void*)callback); + free((void*)responseMessage); + }); + + msg( c("NSApp"), s("runModalForWindow:"), app->mainWindow); + ); } // SaveDialog opens a dialog to select files/directories void SaveDialog(struct Application *app, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories) { - Debug(app, "SaveDialog Called with callback id: %s", callbackID); + Debug(app, "SaveDialog Called with callback id: %s", callbackID); - // Create an open panel - ON_MAIN_THREAD( + // Create an open panel + ON_MAIN_THREAD( - // Create the dialog - id dialog = msg(c("NSSavePanel"), s("savePanel")); + // Create the dialog + id dialog = msg(c("NSSavePanel"), s("savePanel")); - // Valid but appears to do nothing.... :/ - msg(dialog, s("setTitle:"), str(title)); + // Valid but appears to do nothing.... :/ + msg(dialog, s("setTitle:"), str(title)); - // Filters - if( filters != NULL && strlen(filters) > 0) { - id filterString = msg(str(filters), s("stringByReplacingOccurrencesOfString:withString:"), str("*."), str("")); - filterString = msg(filterString, s("stringByReplacingOccurrencesOfString:withString:"), str(" "), str("")); - id filterList = msg(filterString, s("componentsSeparatedByString:"), str(",")); - msg(dialog, s("setAllowedFileTypes:"), filterList); - } else { - msg(dialog, s("setAllowsOtherFileTypes:"), YES); - } + // Filters + if( filters != NULL && strlen(filters) > 0) { + id filterString = msg(str(filters), s("stringByReplacingOccurrencesOfString:withString:"), str("*."), str("")); + filterString = msg(filterString, s("stringByReplacingOccurrencesOfString:withString:"), str(" "), str("")); + id filterList = msg(filterString, s("componentsSeparatedByString:"), str(",")); + msg(dialog, s("setAllowedFileTypes:"), filterList); + } else { + msg(dialog, s("setAllowsOtherFileTypes:"), YES); + } - // Default Directory - if( defaultDir != NULL && strlen(defaultDir) > 0 ) { - msg(dialog, s("setDirectoryURL:"), url(defaultDir)); - } + // Default Directory + if( defaultDir != NULL && strlen(defaultDir) > 0 ) { + msg(dialog, s("setDirectoryURL:"), url(defaultDir)); + } - // Default Filename - if( defaultFilename != NULL && strlen(defaultFilename) > 0 ) { - msg(dialog, s("setNameFieldStringValue:"), str(defaultFilename)); - } + // Default Filename + if( defaultFilename != NULL && strlen(defaultFilename) > 0 ) { + msg(dialog, s("setNameFieldStringValue:"), str(defaultFilename)); + } - // Setup Options - msg(dialog, s("setShowsHiddenFiles:"), showHiddenFiles); - msg(dialog, s("setCanCreateDirectories:"), canCreateDirectories); - msg(dialog, s("setTreatsFilePackagesAsDirectories:"), treatPackagesAsDirectories); + // Setup Options + msg(dialog, s("setShowsHiddenFiles:"), showHiddenFiles); + msg(dialog, s("setCanCreateDirectories:"), canCreateDirectories); + msg(dialog, s("setTreatsFilePackagesAsDirectories:"), treatPackagesAsDirectories); - // Setup callback handler - msg(dialog, s("beginSheetModalForWindow:completionHandler:"), app->mainWindow, ^(id result) { - - // Default is blank - const char *filename = ""; + // Setup callback handler + msg(dialog, s("beginSheetModalForWindow:completionHandler:"), app->mainWindow, ^(id result) { - // If the user selected some files - if( result == (id)1 ) { - // Grab the URL returned - id url = msg(dialog, s("URL")); - filename = (const char *)msg(msg(url, s("path")), s("UTF8String")); - } + // Default is blank + const char *filename = ""; - // Construct callback message. Format "DS|" - const char *callback = concat("DS", callbackID); - const char *header = concat(callback, "|"); - const char *responseMessage = concat(header, filename); + // If the user selected some files + if( result == (id)1 ) { + // Grab the URL returned + id url = msg(dialog, s("URL")); + filename = (const char *)msg(msg(url, s("path")), s("UTF8String")); + } - // Send message to backend - app->sendMessageToBackend(responseMessage); + // Construct callback message. Format "DS|" + const char *callback = concat("DS", callbackID); + const char *header = concat(callback, "|"); + const char *responseMessage = concat(header, filename); - // Free memory - free((void*)header); - free((void*)callback); - free((void*)responseMessage); - }); + // Send message to backend + app->sendMessageToBackend(responseMessage); - msg( c("NSApp"), s("runModalForWindow:"), app->mainWindow); - ); + // Free memory + free((void*)header); + free((void*)callback); + free((void*)responseMessage); + }); + + msg( c("NSApp"), s("runModalForWindow:"), app->mainWindow); + ); } const char *invoke = "window.external={invoke:function(x){window.webkit.messageHandlers.external.postMessage(x);}};"; @@ -856,198 +866,198 @@ void DisableFrame(struct Application *app) void setMinMaxSize(struct Application *app) { - if (app->maxHeight > 0 && app->maxWidth > 0) - { - msg(app->mainWindow, s("setMaxSize:"), CGSizeMake(app->maxWidth, app->maxHeight)); - } - if (app->minHeight > 0 && app->minWidth > 0) - { - msg(app->mainWindow, s("setMinSize:"), CGSizeMake(app->minWidth, app->minHeight)); - } + if (app->maxHeight > 0 && app->maxWidth > 0) + { + msg(app->mainWindow, s("setMaxSize:"), CGSizeMake(app->maxWidth, app->maxHeight)); + } + if (app->minHeight > 0 && app->minWidth > 0) + { + msg(app->mainWindow, s("setMinSize:"), CGSizeMake(app->minWidth, app->minHeight)); + } } void SetMinWindowSize(struct Application *app, int minWidth, int minHeight) { - app->minWidth = minWidth; - app->minHeight = minHeight; + app->minWidth = minWidth; + app->minHeight = minHeight; - // Apply if the window is created - if( app->mainWindow != NULL ) { - ON_MAIN_THREAD( - setMinMaxSize(app); - ); - } + // Apply if the window is created + if( app->mainWindow != NULL ) { + ON_MAIN_THREAD( + setMinMaxSize(app); + ); + } } void SetMaxWindowSize(struct Application *app, int maxWidth, int maxHeight) { - app->maxWidth = maxWidth; - app->maxHeight = maxHeight; - - // Apply if the window is created - if( app->mainWindow != NULL ) { - ON_MAIN_THREAD( - setMinMaxSize(app); - ); - } + app->maxWidth = maxWidth; + app->maxHeight = maxHeight; + + // Apply if the window is created + if( app->mainWindow != NULL ) { + ON_MAIN_THREAD( + setMinMaxSize(app); + ); + } } void SetDebug(void *applicationPointer, int flag) { - debug = flag; + debug = flag; } // SetMenu sets the initial menu for the application void SetMenu(struct Application *app, const char *menuAsJSON) { - app->menuAsJSON = menuAsJSON; + app->menuAsJSON = menuAsJSON; } void SetBindings(struct Application *app, const char *bindings) { - const char* temp = concat("window.wailsbindings = \"", bindings); - const char* jscall = concat(temp, "\";"); - free((void*)temp); - app->bindings = jscall; + const char* temp = concat("window.wailsbindings = \"", bindings); + const char* jscall = concat(temp, "\";"); + free((void*)temp); + app->bindings = jscall; } void makeWindowBackgroundTranslucent(struct Application *app) { - id contentView = msg(app->mainWindow, s("contentView")); - id effectView = msg(c("NSVisualEffectView"), s("alloc")); - CGRect bounds = GET_BOUNDS(contentView); - effectView = msg(effectView, s("initWithFrame:"), bounds); + id contentView = msg(app->mainWindow, s("contentView")); + id effectView = msg(c("NSVisualEffectView"), s("alloc")); + CGRect bounds = GET_BOUNDS(contentView); + effectView = msg(effectView, s("initWithFrame:"), bounds); - msg(effectView, s("setAutoresizingMask:"), NSViewWidthSizable | NSViewHeightSizable); - msg(effectView, s("setBlendingMode:"), NSVisualEffectBlendingModeBehindWindow); - msg(effectView, s("setState:"), NSVisualEffectStateActive); - msg(contentView, s("addSubview:positioned:relativeTo:"), effectView, NSWindowBelow, NULL); - - app->vibrancyLayer = effectView; - Debug(app, "effectView: %p", effectView); + msg(effectView, s("setAutoresizingMask:"), NSViewWidthSizable | NSViewHeightSizable); + msg(effectView, s("setBlendingMode:"), NSVisualEffectBlendingModeBehindWindow); + msg(effectView, s("setState:"), NSVisualEffectStateActive); + msg(contentView, s("addSubview:positioned:relativeTo:"), effectView, NSWindowBelow, NULL); + + app->vibrancyLayer = effectView; + Debug(app, "effectView: %p", effectView); } void enableBoolConfig(id config, const char *setting) { - msg(msg(config, s("preferences")), s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 1), str(setting)); + msg(msg(config, s("preferences")), s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 1), str(setting)); } void disableBoolConfig(id config, const char *setting) { - msg(msg(config, s("preferences")), s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 0), str(setting)); + msg(msg(config, s("preferences")), s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 0), str(setting)); } void processDecorations(struct Application *app) { - - int decorations = 0; - if (app->frame == 1 ) { - if( app->hideTitleBar == 0) { - decorations |= NSWindowStyleMaskTitled; - } - decorations |= NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; - } + int decorations = 0; - if (app->resizable) { - decorations |= NSWindowStyleMaskResizable; - } + if (app->frame == 1 ) { + if( app->hideTitleBar == 0) { + decorations |= NSWindowStyleMaskTitled; + } + decorations |= NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; + } - if (app->fullscreen) { - decorations |= NSWindowStyleMaskFullscreen; - } + if (app->resizable) { + decorations |= NSWindowStyleMaskResizable; + } - if( app->fullSizeContent || app->frame == 0) { - decorations |= NSWindowStyleMaskFullSizeContentView; - } + if (app->fullscreen) { + decorations |= NSWindowStyleMaskFullscreen; + } - app->decorations = decorations; + if( app->fullSizeContent || app->frame == 0) { + decorations |= NSWindowStyleMaskFullSizeContentView; + } + + app->decorations = decorations; } void createApplication(struct Application *app) { - id application = msg(c("NSApplication"), s("sharedApplication")); - app->application = application; - msg(application, s("setActivationPolicy:"), 0); + id application = msg(c("NSApplication"), s("sharedApplication")); + app->application = application; + msg(application, s("setActivationPolicy:"), 0); } void DarkModeEnabled(struct Application *app, const char *callbackID) { - ON_MAIN_THREAD( - const char *result = isDarkMode(app) ? "T" : "F"; + ON_MAIN_THREAD( + const char *result = isDarkMode(app) ? "T" : "F"; - // Construct callback message. Format "SD|" - const char *callback = concat("SD", callbackID); - const char *header = concat(callback, "|"); - const char *responseMessage = concat(header, result); - // Send message to backend - app->sendMessageToBackend(responseMessage); + // Construct callback message. Format "SD|" + const char *callback = concat("SD", callbackID); + const char *header = concat(callback, "|"); + const char *responseMessage = concat(header, result); + // Send message to backend + app->sendMessageToBackend(responseMessage); - // Free memory - free((void*)header); - free((void*)callback); - free((void*)responseMessage); - ); + // Free memory + free((void*)header); + free((void*)callback); + free((void*)responseMessage); + ); } void createDelegate(struct Application *app) { - // Define delegate - Class delegateClass = objc_allocateClassPair((Class) c("NSObject"), "AppDelegate", 0); + // Define delegate + Class delegateClass = objc_allocateClassPair((Class) c("NSObject"), "AppDelegate", 0); 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("applicationShouldTerminateAfterLastWindowClosed:"), (IMP) yes, "c@:@"); + class_addMethod(delegateClass, s("applicationWillTerminate:"), (IMP) closeWindow, "v@:@"); - // Menu Callbacks - class_addMethod(delegateClass, s("menuCallback:"), (IMP)menuItemPressed, "v@:@"); - class_addMethod(delegateClass, s("checkboxMenuCallback:"), (IMP)checkboxMenuItemPressed, "v@:@"); - class_addMethod(delegateClass, s("radioMenuCallback:"), (IMP)radioMenuItemPressed, "v@:@"); + // Menu Callbacks + class_addMethod(delegateClass, s("menuCallback:"), (IMP)menuItemPressed, "v@:@"); + class_addMethod(delegateClass, s("checkboxMenuCallback:"), (IMP)checkboxMenuItemPressed, "v@:@"); + class_addMethod(delegateClass, s("radioMenuCallback:"), (IMP)radioMenuItemPressed, "v@:@"); - // Script handler - class_addMethod(delegateClass, s("userContentController:didReceiveScriptMessage:"), (IMP) messageHandler, "v@:@@"); - objc_registerClassPair(delegateClass); + // Script handler + class_addMethod(delegateClass, s("userContentController:didReceiveScriptMessage:"), (IMP) messageHandler, "v@:@@"); + objc_registerClassPair(delegateClass); - // Create delegate - id delegate = msg((id)delegateClass, s("new")); - objc_setAssociatedObject(delegate, "application", (id)app, OBJC_ASSOCIATION_ASSIGN); + // Create delegate + id delegate = msg((id)delegateClass, s("new")); + objc_setAssociatedObject(delegate, "application", (id)app, OBJC_ASSOCIATION_ASSIGN); - // Theme change listener - class_addMethod(delegateClass, s("themeChanged:"), (IMP) themeChanged, "v@:@@"); + // Theme change listener + class_addMethod(delegateClass, s("themeChanged:"), (IMP) themeChanged, "v@:@@"); - // Get defaultCenter - id defaultCenter = msg(c("NSDistributedNotificationCenter"), s("defaultCenter")); - msg(defaultCenter, s("addObserver:selector:name:object:"), delegate, s("themeChanged:"), str("AppleInterfaceThemeChangedNotification"), NULL); + // Get defaultCenter + id defaultCenter = msg(c("NSDistributedNotificationCenter"), s("defaultCenter")); + msg(defaultCenter, s("addObserver:selector:name:object:"), delegate, s("themeChanged:"), str("AppleInterfaceThemeChangedNotification"), NULL); - app->delegate = delegate; + app->delegate = delegate; - msg(app->application, s("setDelegate:"), delegate); + msg(app->application, s("setDelegate:"), delegate); } void createMainWindow(struct Application *app) { - // Create main window - id mainWindow = ALLOC("NSWindow"); - mainWindow = msg(mainWindow, s("initWithContentRect:styleMask:backing:defer:"), - CGRectMake(0, 0, app->width, app->height), app->decorations, NSBackingStoreBuffered, NO); - msg(mainWindow, s("autorelease")); + // Create main window + id mainWindow = ALLOC("NSWindow"); + mainWindow = msg(mainWindow, s("initWithContentRect:styleMask:backing:defer:"), + CGRectMake(0, 0, app->width, app->height), app->decorations, NSBackingStoreBuffered, NO); + msg(mainWindow, s("autorelease")); - // Set Appearance - if( app->appearance != NULL ) { - msg(mainWindow, s("setAppearance:"), - msg(c("NSAppearance"), s("appearanceNamed:"), str(app->appearance)) - ); - } + // Set Appearance + if( app->appearance != NULL ) { + msg(mainWindow, s("setAppearance:"), + msg(c("NSAppearance"), s("appearanceNamed:"), str(app->appearance)) + ); + } - // Set Title appearance - msg(mainWindow, s("setTitlebarAppearsTransparent:"), app->titlebarAppearsTransparent ? YES : NO); - msg(mainWindow, s("setTitleVisibility:"), app->hideTitle); + // Set Title appearance + msg(mainWindow, s("setTitlebarAppearsTransparent:"), app->titlebarAppearsTransparent ? YES : NO); + msg(mainWindow, s("setTitleVisibility:"), app->hideTitle); - app->mainWindow = mainWindow; + app->mainWindow = mainWindow; } const char* getInitialState(struct Application *app) { - const char *result = ""; - if( isDarkMode(app) ) { - result = "window.wails.System.IsDarkMode.set(true);"; - } else { - result = "window.wails.System.IsDarkMode.set(false);"; - } - char buffer[999]; - snprintf(&buffer[0], sizeof(buffer), "window.wails.System.LogLevel.set(%d);", app->logLevel); - result = concat(result, &buffer[0]); - Debug(app, "initialstate = %s", result); - return result; + const char *result = ""; + if( isDarkMode(app) ) { + result = "window.wails.System.IsDarkMode.set(true);"; + } else { + result = "window.wails.System.IsDarkMode.set(false);"; + } + char buffer[999]; + snprintf(&buffer[0], sizeof(buffer), "window.wails.System.LogLevel.set(%d);", app->logLevel); + result = concat(result, &buffer[0]); + Debug(app, "initialstate = %s", result); + return result; } id createMenuItem(id title, const char *action, const char *key) { @@ -1072,10 +1082,10 @@ id createMenu(id title) { } id addMenuItem(id menu, const char *title, const char *action, const char *key, bool disabled) { - id item = createMenuItem(str(title), action, key); - msg(item, s("setEnabled:"), !disabled); - msg(menu, s("addItem:"), item); - return item; + id item = createMenuItem(str(title), action, key); + msg(item, s("setEnabled:"), !disabled); + msg(menu, s("addItem:"), item); + return item; } void addSeparator(id menu) { @@ -1130,77 +1140,77 @@ void parseMenuRole(struct Application *app, id parentMenu, JsonNode *item) { const char *roleName = item->string_; if ( STREQ(roleName, "appMenu") ) { - createDefaultAppMenu(parentMenu); - return; + createDefaultAppMenu(parentMenu); + return; } if ( STREQ(roleName, "editMenu")) { - createDefaultEditMenu(parentMenu); - return; + createDefaultEditMenu(parentMenu); + return; } if ( STREQ(roleName, "hide")) { - addMenuItem(parentMenu, "Hide Window", "hide:", "h", FALSE); - return; + addMenuItem(parentMenu, "Hide Window", "hide:", "h", FALSE); + return; } if ( STREQ(roleName, "hideothers")) { - id hideOthers = addMenuItem(parentMenu, "Hide Others", "hideOtherApplications:", "h", FALSE); - msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand)); - return; + id hideOthers = addMenuItem(parentMenu, "Hide Others", "hideOtherApplications:", "h", FALSE); + msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand)); + return; } if ( STREQ(roleName, "unhide")) { - addMenuItem(parentMenu, "Show All", "unhideAllApplications:", "", FALSE); - return; + addMenuItem(parentMenu, "Show All", "unhideAllApplications:", "", FALSE); + return; } if ( STREQ(roleName, "front")) { - addMenuItem(parentMenu, "Bring All to Front", "arrangeInFront:", "", FALSE); - return; + addMenuItem(parentMenu, "Bring All to Front", "arrangeInFront:", "", FALSE); + return; } if ( STREQ(roleName, "undo")) { - addMenuItem(parentMenu, "Undo", "undo:", "z", FALSE); - return; + addMenuItem(parentMenu, "Undo", "undo:", "z", FALSE); + return; } if ( STREQ(roleName, "redo")) { - addMenuItem(parentMenu, "Redo", "redo:", "y", FALSE); - return; + addMenuItem(parentMenu, "Redo", "redo:", "y", FALSE); + return; } if ( STREQ(roleName, "cut")) { - addMenuItem(parentMenu, "Cut", "cut:", "x", FALSE); - return; + addMenuItem(parentMenu, "Cut", "cut:", "x", FALSE); + return; } if ( STREQ(roleName, "copy")) { - addMenuItem(parentMenu, "Copy", "copy:", "c", FALSE); - return; + addMenuItem(parentMenu, "Copy", "copy:", "c", FALSE); + return; } if ( STREQ(roleName, "paste")) { - addMenuItem(parentMenu, "Paste", "paste:", "v", FALSE); - return; + addMenuItem(parentMenu, "Paste", "paste:", "v", FALSE); + return; } if ( STREQ(roleName, "delete")) { - addMenuItem(parentMenu, "Delete", "delete:", "", FALSE); - return; + addMenuItem(parentMenu, "Delete", "delete:", "", FALSE); + return; } if( STREQ(roleName, "pasteandmatchstyle")) { - id pasteandmatchstyle = addMenuItem(parentMenu, "Paste and Match Style", "pasteandmatchstyle:", "v", FALSE); - msg(pasteandmatchstyle, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagCommand)); + id pasteandmatchstyle = addMenuItem(parentMenu, "Paste and Match Style", "pasteandmatchstyle:", "v", FALSE); + msg(pasteandmatchstyle, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagCommand)); } if ( STREQ(roleName, "selectall")) { - addMenuItem(parentMenu, "Select All", "selectAll:", "a", FALSE); - return; + addMenuItem(parentMenu, "Select All", "selectAll:", "a", FALSE); + return; } if ( STREQ(roleName, "minimize")) { - addMenuItem(parentMenu, "Minimize", "miniaturize:", "m", FALSE); - return; + addMenuItem(parentMenu, "Minimize", "miniaturize:", "m", FALSE); + return; } if ( STREQ(roleName, "zoom")) { - addMenuItem(parentMenu, "Zoom", "performZoom:", "", FALSE); - return; + addMenuItem(parentMenu, "Zoom", "performZoom:", "", FALSE); + return; } if ( STREQ(roleName, "quit")) { - addMenuItem(parentMenu, "Quit (More work TBD)", "terminate:", "q", FALSE); - return; + addMenuItem(parentMenu, "Quit (More work TBD)", "terminate:", "q", FALSE); + return; } if ( STREQ(roleName, "togglefullscreen")) { - addMenuItem(parentMenu, "Toggle Full Screen", "toggleFullScreen:", "f", FALSE); - return; + addMenuItem(parentMenu, "Toggle Full Screen", "toggleFullScreen:", "f", FALSE); + return; } } @@ -1210,7 +1220,7 @@ const char* getJSONString(JsonNode *item, const char* key) { JsonNode *node = json_find_member(item, key); const char *result = ""; if ( node != NULL && node->tag == JSON_STRING) { - result = node->string_; + result = node->string_; } return result; } @@ -1218,8 +1228,8 @@ const char* getJSONString(JsonNode *item, const char* key) { bool getJSONBool(JsonNode *item, const char* key, bool *result) { JsonNode *node = json_find_member(item, key); if ( node != NULL && node->tag == JSON_BOOL) { - *result = node->bool_; - return true; + *result = node->bool_; + return true; } return false; } @@ -1227,8 +1237,8 @@ bool getJSONBool(JsonNode *item, const char* key, bool *result) { bool getJSONInt(JsonNode *item, const char* key, int *result) { JsonNode *node = json_find_member(item, key); if ( node != NULL && node->tag == JSON_NUMBER) { - *result = (int) node->number_; - return true; + *result = (int) node->number_; + return true; } return false; } @@ -1243,187 +1253,193 @@ unsigned long parseModifiers(const char **modifiers) { const char *thisModifier = modifiers[0]; int count = 0; while( thisModifier != NULL ) { - // Determine flags - if( STREQ(thisModifier, "CmdOrCtrl") ) { - result |= NSEventModifierFlagCommand; - } - if( STREQ(thisModifier, "OptionOrAlt") ) { - result |= NSEventModifierFlagOption; - } - if( STREQ(thisModifier, "Shift") ) { - result |= NSEventModifierFlagShift; - } - if( STREQ(thisModifier, "Super") ) { - result |= NSEventModifierFlagCommand; - } - if( STREQ(thisModifier, "Control") ) { - result |= NSEventModifierFlagControl; - } - count++; - thisModifier = modifiers[count]; + // Determine flags + if( STREQ(thisModifier, "CmdOrCtrl") ) { + result |= NSEventModifierFlagCommand; + } + if( STREQ(thisModifier, "OptionOrAlt") ) { + result |= NSEventModifierFlagOption; + } + if( STREQ(thisModifier, "Shift") ) { + result |= NSEventModifierFlagShift; + } + if( STREQ(thisModifier, "Super") ) { + result |= NSEventModifierFlagCommand; + } + if( STREQ(thisModifier, "Control") ) { + result |= NSEventModifierFlagControl; + } + count++; + thisModifier = modifiers[count]; } return result; } id processAcceleratorKey(const char *key) { + + // Guard against no accelerator key + if( key == NULL ) { + return str(""); + } + if( STREQ(key, "Backspace") ) { - return strunicode(0x0008); + return strunicode(0x0008); } if( STREQ(key, "Tab") ) { - return strunicode(0x0009); + return strunicode(0x0009); } if( STREQ(key, "Return") ) { - return strunicode(0x000d); + return strunicode(0x000d); } if( STREQ(key, "Escape") ) { - return strunicode(0x001b); + return strunicode(0x001b); } if( STREQ(key, "Left") ) { - return strunicode(0x001c); + return strunicode(0x001c); } if( STREQ(key, "Right") ) { - return strunicode(0x001d); + return strunicode(0x001d); } if( STREQ(key, "Up") ) { - return strunicode(0x001e); + return strunicode(0x001e); } if( STREQ(key, "Down") ) { - return strunicode(0x001f); + return strunicode(0x001f); } if( STREQ(key, "Space") ) { - return strunicode(0x0020); + return strunicode(0x0020); } if( STREQ(key, "Delete") ) { - return strunicode(0x007f); + return strunicode(0x007f); } if( STREQ(key, "Home") ) { - return strunicode(0x2196); + return strunicode(0x2196); } if( STREQ(key, "End") ) { - return strunicode(0x2198); + return strunicode(0x2198); } if( STREQ(key, "Page Up") ) { - return strunicode(0x21de); + return strunicode(0x21de); } if( STREQ(key, "Page Down") ) { - return strunicode(0x21df); + return strunicode(0x21df); } if( STREQ(key, "F1") ) { - return strunicode(0xf704); + return strunicode(0xf704); } if( STREQ(key, "F2") ) { - return strunicode(0xf705); + return strunicode(0xf705); } if( STREQ(key, "F3") ) { - return strunicode(0xf706); + return strunicode(0xf706); } if( STREQ(key, "F4") ) { - return strunicode(0xf707); + return strunicode(0xf707); } if( STREQ(key, "F5") ) { - return strunicode(0xf708); + return strunicode(0xf708); } if( STREQ(key, "F6") ) { - return strunicode(0xf709); + return strunicode(0xf709); } if( STREQ(key, "F7") ) { - return strunicode(0xf70a); + return strunicode(0xf70a); } if( STREQ(key, "F8") ) { - return strunicode(0xf70b); + return strunicode(0xf70b); } if( STREQ(key, "F9") ) { - return strunicode(0xf70c); + return strunicode(0xf70c); } if( STREQ(key, "F10") ) { - return strunicode(0xf70d); + return strunicode(0xf70d); } if( STREQ(key, "F11") ) { - return strunicode(0xf70e); + return strunicode(0xf70e); } if( STREQ(key, "F12") ) { - return strunicode(0xf70f); + return strunicode(0xf70f); } if( STREQ(key, "F13") ) { - return strunicode(0xf710); + return strunicode(0xf710); } if( STREQ(key, "F14") ) { - return strunicode(0xf711); + return strunicode(0xf711); } if( STREQ(key, "F15") ) { - return strunicode(0xf712); + return strunicode(0xf712); } if( STREQ(key, "F16") ) { - return strunicode(0xf713); + return strunicode(0xf713); } if( STREQ(key, "F17") ) { - return strunicode(0xf714); + return strunicode(0xf714); } if( STREQ(key, "F18") ) { - return strunicode(0xf715); + return strunicode(0xf715); } if( STREQ(key, "F19") ) { - return strunicode(0xf716); + return strunicode(0xf716); } if( STREQ(key, "F20") ) { - return strunicode(0xf717); + return strunicode(0xf717); } if( STREQ(key, "F21") ) { - return strunicode(0xf718); + return strunicode(0xf718); } if( STREQ(key, "F22") ) { - return strunicode(0xf719); + return strunicode(0xf719); } if( STREQ(key, "F23") ) { - return strunicode(0xf71a); + return strunicode(0xf71a); } if( STREQ(key, "F24") ) { - return strunicode(0xf71b); + return strunicode(0xf71b); } if( STREQ(key, "F25") ) { - return strunicode(0xf71c); + return strunicode(0xf71c); } if( STREQ(key, "F26") ) { - return strunicode(0xf71d); + return strunicode(0xf71d); } if( STREQ(key, "F27") ) { - return strunicode(0xf71e); + return strunicode(0xf71e); } if( STREQ(key, "F28") ) { - return strunicode(0xf71f); + return strunicode(0xf71f); } if( STREQ(key, "F29") ) { - return strunicode(0xf720); + return strunicode(0xf720); } if( STREQ(key, "F30") ) { - return strunicode(0xf721); + return strunicode(0xf721); } if( STREQ(key, "F31") ) { - return strunicode(0xf722); + return strunicode(0xf722); } if( STREQ(key, "F32") ) { - return strunicode(0xf723); + return strunicode(0xf723); } if( STREQ(key, "F33") ) { - return strunicode(0xf724); + return strunicode(0xf724); } if( STREQ(key, "F34") ) { - return strunicode(0xf725); + return strunicode(0xf725); } if( STREQ(key, "F35") ) { - return strunicode(0xf726); + return strunicode(0xf726); } if( STREQ(key, "Insert") ) { - return strunicode(0xf727); + return strunicode(0xf727); } if( STREQ(key, "PrintScreen") ) { - return strunicode(0xf72e); + return strunicode(0xf72e); } if( STREQ(key, "ScrollLock") ) { - return strunicode(0xf72f); + return strunicode(0xf72f); } if( STREQ(key, "NumLock") ) { - return strunicode(0xf739); + return strunicode(0xf739); } return str(key); @@ -1431,58 +1447,61 @@ id processAcceleratorKey(const char *key) { id parseTextMenuItem(struct Application *app, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers) { - id item = ALLOC("NSMenuItem"); - id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), menuid); - msg(item, s("setRepresentedObject:"), wrappedId); + id item = ALLOC("NSMenuItem"); + id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), menuid); + msg(item, s("setRepresentedObject:"), wrappedId); - id key = processAcceleratorKey(acceleratorkey); - msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuCallback:"), key); - msg(item, s("setEnabled:"), !disabled); - msg(item, s("autorelease")); + id key = processAcceleratorKey(acceleratorkey); + msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), + s("menuCallback:"), key); - // Process modifiers - if( modifiers != NULL ) { - unsigned long modifierFlags = parseModifiers(modifiers); - msg(item, s("setKeyEquivalentModifierMask:"), modifierFlags); - } + msg(item, s("setEnabled:"), !disabled); + msg(item, s("autorelease")); - msg(parentMenu, s("addItem:"), item); + // Process modifiers + if( modifiers != NULL ) { + unsigned long modifierFlags = parseModifiers(modifiers); + msg(item, s("setKeyEquivalentModifierMask:"), modifierFlags); + } + msg(parentMenu, s("addItem:"), item); - return item; + return item; } id parseCheckboxMenuItem(struct Application *app, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *key) { - id item = ALLOC("NSMenuItem"); + id item = ALLOC("NSMenuItem"); - // Store the item in the menu item map - hashmap_put(&menuItemMap, (char*)menuid, strlen(menuid), item); - - id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), menuid); - msg(item, s("setRepresentedObject:"), wrappedId); - msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("checkboxMenuCallback:"), str(key)); - msg(item, s("setEnabled:"), !disabled); - msg(item, s("autorelease")); - msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff)); - msg(parentmenu, s("addItem:"), item); - return item; + // Store the item in the menu item map + hashmap_put(&menuItemMap, (char*)menuid, strlen(menuid), item); + id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), menuid); + msg(item, s("setRepresentedObject:"), wrappedId); + msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("checkboxMenuCallback:"), str(key)); + msg(item, s("setEnabled:"), !disabled); + msg(item, s("autorelease")); + msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff)); + msg(parentmenu, s("addItem:"), item); + return item; } id parseRadioMenuItem(struct Application *app, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *acceleratorkey) { - id item = ALLOC("NSMenuItem"); + id item = ALLOC("NSMenuItem"); - // Store the item in the menu item map - hashmap_put(&menuItemMap, (char*)menuid, strlen(menuid), item); + // Store the item in the menu item map + hashmap_put(&menuItemMap, (char*)menuid, strlen(menuid), item); - id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), menuid); - msg(item, s("setRepresentedObject:"), wrappedId); - id key = processAcceleratorKey(acceleratorkey); - msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("radioMenuCallback:"), key); - msg(item, s("setEnabled:"), !disabled); - msg(item, s("autorelease")); - msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff)); - msg(parentmenu, s("addItem:"), item); - return item; + id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), menuid); + msg(item, s("setRepresentedObject:"), wrappedId); + + id key = processAcceleratorKey(acceleratorkey); + msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("radioMenuCallback:"), key); + + msg(item, s("setEnabled:"), !disabled); + msg(item, s("autorelease")); + msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff)); + + msg(parentmenu, s("addItem:"), item); + return item; } @@ -1492,52 +1511,52 @@ void parseMenuItem(struct Application *app, id parentMenu, JsonNode *item) { bool hidden = false; getJSONBool(item, "Hidden", &hidden); if( hidden ) { - return; + return; } // Get the role JsonNode *role = json_find_member(item, "Role"); if( role != NULL ) { - parseMenuRole(app, parentMenu, role); - return; + parseMenuRole(app, parentMenu, role); + return; } // Check if this is a submenu JsonNode *submenu = json_find_member(item, "SubMenu"); if( submenu != NULL ) { - // Get the label - JsonNode *menuNameNode = json_find_member(item, "Label"); - const char *name = ""; - if ( menuNameNode != NULL) { - name = menuNameNode->string_; - } - - id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, ""); - id thisMenu = createMenu(str(name)); + // Get the label + JsonNode *menuNameNode = json_find_member(item, "Label"); + const char *name = ""; + if ( menuNameNode != NULL) { + name = menuNameNode->string_; + } - msg(thisMenuItem, s("setSubmenu:"), thisMenu); - msg(parentMenu, s("addItem:"), thisMenuItem); + id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, ""); + id thisMenu = createMenu(str(name)); - // Loop over submenu items - JsonNode *item; - json_foreach(item, submenu) { - // Get item label - parseMenuItem(app, thisMenu, item); - } + msg(thisMenuItem, s("setSubmenu:"), thisMenu); + msg(parentMenu, s("addItem:"), thisMenuItem); - return; + // Loop over submenu items + JsonNode *item; + json_foreach(item, submenu) { + // Get item label + parseMenuItem(app, thisMenu, item); + } + + return; } // This is a user menu. Get the common data // Get the label const char *label = getJSONString(item, "Label"); if ( label == NULL) { - label = "(empty)"; + label = "(empty)"; } const char *menuid = getJSONString(item, "ID"); if ( menuid == NULL) { - menuid = ""; + menuid = ""; } bool disabled = false; @@ -1550,25 +1569,25 @@ void parseMenuItem(struct Application *app, id parentMenu, JsonNode *item) { // If we have an accelerator if( accelerator != NULL ) { - // Get the key - acceleratorkey = getJSONString(accelerator, "Key"); - // Check if there are modifiers - JsonNode *modifiersList = json_find_member(accelerator, "Modifiers"); - if ( modifiersList != NULL ) { - // Allocate an array of strings - int noOfModifiers = json_array_length(modifiersList); - modifiers = malloc(sizeof(const char *) * (noOfModifiers + 1)); - JsonNode *modifier; - int count = 0; - // Iterate the modifiers and save a reference to them in our new array - json_foreach(modifier, modifiersList) { - // Get modifier name - modifiers[count] = modifier->string_; - count++; - } - // Null terminate the modifier list - modifiers[count] = NULL; - } + // Get the key + acceleratorkey = getJSONString(accelerator, "Key"); + // Check if there are modifiers + JsonNode *modifiersList = json_find_member(accelerator, "Modifiers"); + if ( modifiersList != NULL ) { + // Allocate an array of strings + int noOfModifiers = json_array_length(modifiersList); + modifiers = malloc(sizeof(const char *) * (noOfModifiers + 1)); + JsonNode *modifier; + int count = 0; + // Iterate the modifiers and save a reference to them in our new array + json_foreach(modifier, modifiersList) { + // Get modifier name + modifiers[count] = modifier->string_; + count++; + } + // Null terminate the modifier list + modifiers[count] = NULL; + } } @@ -1576,48 +1595,48 @@ void parseMenuItem(struct Application *app, id parentMenu, JsonNode *item) { JsonNode *type = json_find_member(item, "Type"); if( type != NULL ) { - if( STREQ(type->string_, "Text")) { - parseTextMenuItem(app, parentMenu, label, menuid, disabled, acceleratorkey, modifiers); - } - else if ( STREQ(type->string_, "Separator")) { - addSeparator(parentMenu); - } - else if ( STREQ(type->string_, "Checkbox")) { - // Get checked state - bool checked = false; - getJSONBool(item, "Checked", &checked); + if( STREQ(type->string_, "Text")) { + parseTextMenuItem(app, parentMenu, label, menuid, disabled, acceleratorkey, modifiers); + } + else if ( STREQ(type->string_, "Separator")) { + addSeparator(parentMenu); + } + else if ( STREQ(type->string_, "Checkbox")) { + // Get checked state + bool checked = false; + getJSONBool(item, "Checked", &checked); - parseCheckboxMenuItem(app, parentMenu, label, menuid, disabled, checked, ""); - } - else if ( STREQ(type->string_, "Radio")) { - // Get checked state - bool checked = false; - getJSONBool(item, "Checked", &checked); + parseCheckboxMenuItem(app, parentMenu, label, menuid, disabled, checked, ""); + } + else if ( STREQ(type->string_, "Radio")) { + // Get checked state + bool checked = false; + getJSONBool(item, "Checked", &checked); - parseRadioMenuItem(app, parentMenu, label, menuid, disabled, checked, ""); - } + parseRadioMenuItem(app, parentMenu, label, menuid, disabled, checked, ""); + } - if ( modifiers != NULL ) { - free(modifiers); - } + if ( modifiers != NULL ) { + free(modifiers); + } - return; + return; } } void parseMenu(struct Application *app, id parentMenu, JsonNode *menu) { JsonNode *items = json_find_member(menu, "Items"); if( items == NULL ) { - // Parse error! - Fatal(app, "Unable to find Items:", app->menuAsJSON); - return; + // Parse error! + Fatal(app, "Unable to find Items:", app->menuAsJSON); + return; } // Iterate items JsonNode *item; json_foreach(item, items) { - // Get item label - parseMenuItem(app, parentMenu, item); + // Get item label + parseMenuItem(app, parentMenu, item); } } @@ -1626,9 +1645,9 @@ void dumpMemberList(const char *name, id *memberList) { int count = 0; printf("%s = %p -> [ ", name, memberList); while( member != NULL ) { - printf("%p ", member); - count = count + 1; - member = memberList[count]; + printf("%p ", member); + count = count + 1; + member = memberList[count]; } printf("]\n"); } @@ -1647,11 +1666,11 @@ void processRadioGroup(JsonNode *radioGroup) { // Build the radio group items int count=0; json_foreach(member, members) { - // Get menu by id - id menuItem = (id)hashmap_get(&menuItemMap, (char*)member->string_, strlen(member->string_)); - // Save Member - memberList[count] = menuItem; - count = count + 1; + // Get menu by id + id menuItem = (id)hashmap_get(&menuItemMap, (char*)member->string_, strlen(member->string_)); + // Save Member + memberList[count] = menuItem; + count = count + 1; } // Null terminate array memberList[groupLength] = 0; @@ -1660,14 +1679,14 @@ void processRadioGroup(JsonNode *radioGroup) { // Store the members json_foreach(member, members) { - // Copy the memberList - char *newMemberList = (char *)malloc(arrayLength); - memcpy(newMemberList, memberList, arrayLength); - // dumpMemberList("newMemberList", newMemberList); - // printf("Address of newMemberList = %p\n", newMemberList); + // Copy the memberList + char *newMemberList = (char *)malloc(arrayLength); + memcpy(newMemberList, memberList, arrayLength); + // dumpMemberList("newMemberList", newMemberList); + // printf("Address of newMemberList = %p\n", newMemberList); - // add group to each member of group - hashmap_put(&radioGroupMap, member->string_, strlen(member->string_), newMemberList); + // add group to each member of group + hashmap_put(&radioGroupMap, member->string_, strlen(member->string_), newMemberList); } // dumpHashmap("radioGroupMap", &radioGroupMap); @@ -1676,218 +1695,243 @@ void processRadioGroup(JsonNode *radioGroup) { void parseMenuData(struct Application *app) { - // Create a new menu bar - id menubar = createMenu(str("")); + // Allocate the hashmaps we need + allocateHashMaps(app); - // Parse the processed menu json - app->processedMenu = json_decode(app->menuAsJSON); - - if( app->processedMenu == NULL ) { - // Parse error! - Fatal(app, "Unable to parse Menu JSON: %s", app->menuAsJSON); - return; - } + // Create a new menu bar + id menubar = createMenu(str("")); - // Pull out the Menu - JsonNode *menuData = json_find_member(app->processedMenu, "Menu"); - if( menuData == NULL ) { - // Parse error! - Fatal(app, "Unable to find Menu data: %s", app->processedMenu); - return; - } + // Parse the processed menu json + app->processedMenu = json_decode(app->menuAsJSON); - parseMenu(app, menubar, menuData); + if( app->processedMenu == NULL ) { + // Parse error! + Fatal(app, "Unable to parse Menu JSON: %s", app->menuAsJSON); + return; + } - // Create the radiogroup cache - JsonNode *radioGroups = json_find_member(app->processedMenu, "RadioGroups"); - if( radioGroups == NULL ) { - // Parse error! - Fatal(app, "Unable to find RadioGroups data: %s", app->processedMenu); - return; - } - // Iterate radio groups - JsonNode *radioGroup; - json_foreach(radioGroup, radioGroups) { - // Get item label - processRadioGroup(radioGroup); - } + // Pull out the Menu + JsonNode *menuData = json_find_member(app->processedMenu, "Menu"); + if( menuData == NULL ) { + // Parse error! + Fatal(app, "Unable to find Menu data: %s", app->processedMenu); + return; + } - // Apply the menu bar - msg(msg(c("NSApplication"), s("sharedApplication")), s("setMainMenu:"), menubar); + parseMenu(app, menubar, menuData); + + // Create the radiogroup cache + JsonNode *radioGroups = json_find_member(app->processedMenu, "RadioGroups"); + if( radioGroups == NULL ) { + // Parse error! + Fatal(app, "Unable to find RadioGroups data: %s", app->processedMenu); + return; + } + + // Iterate radio groups + JsonNode *radioGroup; + json_foreach(radioGroup, radioGroups) { + // Get item label + processRadioGroup(radioGroup); + } + + // Apply the menu bar + msg(msg(c("NSApplication"), s("sharedApplication")), s("setMainMenu:"), menubar); + +} + + +// UpdateMenu replaces the current menu with the given one +void UpdateMenu(struct Application *app, const char *menuAsJSON) { + Debug(app, "Menu is now: %s", menuAsJSON); + ON_MAIN_THREAD ( + + // Remove the current Menu + id menubar = msg(msg(c("NSApplication"), s("sharedApplication")), s("mainMenu")); + Debug(app, "Got menubar: %p", menubar); + msg(menubar, s("removeAllItems")); + + // Free up memory + destroyMenu(app); + + // Set the menu JSON + app->menuAsJSON = menuAsJSON; + parseMenuData(app); + ); } void Run(struct Application *app, int argc, char **argv) { - processDecorations(app); + processDecorations(app); - createApplication(app); + createApplication(app); - // Define delegate - createDelegate(app); + // Define delegate + createDelegate(app); - createMainWindow(app); + createMainWindow(app); - // Create Content View - id contentView = msg( ALLOC("NSView"), s("init") ); - msg(app->mainWindow, s("setContentView:"), contentView); + // Create Content View + id contentView = msg( ALLOC("NSView"), s("init") ); + msg(app->mainWindow, s("setContentView:"), contentView); - // Set the main window title - SetTitle(app, app->title); + // Set the main window title + SetTitle(app, app->title); - // Center Window - Center(app); + // Center Window + Center(app); - // Set Colour - applyWindowColour(app); + // Set Colour + applyWindowColour(app); - if (app->windowBackgroundIsTranslucent) { - makeWindowBackgroundTranslucent(app); - } + if (app->windowBackgroundIsTranslucent) { + makeWindowBackgroundTranslucent(app); + } - // Setup webview - id config = msg(c("WKWebViewConfiguration"), s("new")); - msg(config, s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 1), str("suppressesIncrementalRendering")); - if (app->devtools) { - Debug(app, "Enabling devtools"); - enableBoolConfig(config, "developerExtrasEnabled"); - } - app->config = config; + // Setup webview + id config = msg(c("WKWebViewConfiguration"), s("new")); + msg(config, s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 1), str("suppressesIncrementalRendering")); + if (app->devtools) { + Debug(app, "Enabling devtools"); + enableBoolConfig(config, "developerExtrasEnabled"); + } + app->config = config; - id manager = msg(config, s("userContentController")); - msg(manager, s("addScriptMessageHandler:name:"), app->delegate, str("external")); - msg(manager, s("addScriptMessageHandler:name:"), app->delegate, str("completed")); - app->manager = manager; + id manager = msg(config, s("userContentController")); + msg(manager, s("addScriptMessageHandler:name:"), app->delegate, str("external")); + msg(manager, s("addScriptMessageHandler:name:"), app->delegate, str("completed")); + app->manager = manager; - id wkwebview = msg(c("WKWebView"), s("alloc")); - app->wkwebview = wkwebview; + id wkwebview = msg(c("WKWebView"), s("alloc")); + app->wkwebview = wkwebview; - msg(wkwebview, s("initWithFrame:configuration:"), CGRectMake(0, 0, 0, 0), config); + msg(wkwebview, s("initWithFrame:configuration:"), CGRectMake(0, 0, 0, 0), config); - msg(contentView, s("addSubview:"), wkwebview); - msg(wkwebview, s("setAutoresizingMask:"), NSViewWidthSizable | NSViewHeightSizable); - CGRect contentViewBounds = GET_BOUNDS(contentView); - msg(wkwebview, s("setFrame:"), contentViewBounds ); + msg(contentView, s("addSubview:"), wkwebview); + msg(wkwebview, s("setAutoresizingMask:"), NSViewWidthSizable | NSViewHeightSizable); + CGRect contentViewBounds = GET_BOUNDS(contentView); + msg(wkwebview, s("setFrame:"), contentViewBounds ); - // Disable damn smart quotes - // Credit: https://stackoverflow.com/a/31640511 - id userDefaults = msg(c("NSUserDefaults"), s("standardUserDefaults")); - msg(userDefaults, s("setBool:forKey:"), NO, str("NSAutomaticQuoteSubstitutionEnabled")); + // Disable damn smart quotes + // Credit: https://stackoverflow.com/a/31640511 + id userDefaults = msg(c("NSUserDefaults"), s("standardUserDefaults")); + msg(userDefaults, s("setBool:forKey:"), NO, str("NSAutomaticQuoteSubstitutionEnabled")); - // Setup drag message handler - msg(manager, s("addScriptMessageHandler:name:"), app->delegate, str("windowDrag")); - // Add mouse event hooks - app->mouseDownMonitor = msg(c("NSEvent"), u("addLocalMonitorForEventsMatchingMask:handler:"), NSEventMaskLeftMouseDown, ^(id incomingEvent) { - app->mouseEvent = incomingEvent; - return incomingEvent; - }); - app->mouseUpMonitor = msg(c("NSEvent"), u("addLocalMonitorForEventsMatchingMask:handler:"), NSEventMaskLeftMouseUp, ^(id incomingEvent) { - app->mouseEvent = NULL; - if ( app->dragging ) { - ShowMouse(); - app->dragging = false; - } - return incomingEvent; - }); + // Setup drag message handler + msg(manager, s("addScriptMessageHandler:name:"), app->delegate, str("windowDrag")); + // Add mouse event hooks + app->mouseDownMonitor = msg(c("NSEvent"), u("addLocalMonitorForEventsMatchingMask:handler:"), NSEventMaskLeftMouseDown, ^(id incomingEvent) { + app->mouseEvent = incomingEvent; + return incomingEvent; + }); + app->mouseUpMonitor = msg(c("NSEvent"), u("addLocalMonitorForEventsMatchingMask:handler:"), NSEventMaskLeftMouseUp, ^(id incomingEvent) { + app->mouseEvent = NULL; + if ( app->dragging ) { + ShowMouse(); + app->dragging = false; + } + return incomingEvent; + }); - // Toolbar - if( app->useToolBar ) { - Debug(app, "Setting Toolbar"); - id toolbar = msg(c("NSToolbar"),s("alloc")); - msg(toolbar, s("initWithIdentifier:"), str("wails.toolbar")); - msg(toolbar, s("autorelease")); + // Toolbar + if( app->useToolBar ) { + Debug(app, "Setting Toolbar"); + id toolbar = msg(c("NSToolbar"),s("alloc")); + msg(toolbar, s("initWithIdentifier:"), str("wails.toolbar")); + msg(toolbar, s("autorelease")); - // Separator - if( app->hideToolbarSeparator ) { - msg(toolbar, s("setShowsBaselineSeparator:"), NO); - } + // Separator + if( app->hideToolbarSeparator ) { + msg(toolbar, s("setShowsBaselineSeparator:"), NO); + } - msg(app->mainWindow, s("setToolbar:"), toolbar); - } + msg(app->mainWindow, s("setToolbar:"), toolbar); + } - // Fix up resizing - if (app->resizable == 0) { - app->minHeight = app->maxHeight = app->height; - app->minWidth = app->maxWidth = app->width; - } - setMinMaxSize(app); + // Fix up resizing + if (app->resizable == 0) { + app->minHeight = app->maxHeight = app->height; + app->minWidth = app->maxWidth = app->width; + } + setMinMaxSize(app); - // Load HTML - id html = msg(c("NSURL"), s("URLWithString:"), str(assets[0])); - msg(wkwebview, s("loadRequest:"), msg(c("NSURLRequest"), s("requestWithURL:"), html)); - - Debug(app, "Loading Internal Code"); - // 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); + // Load HTML + id html = msg(c("NSURL"), s("URLWithString:"), str(assets[0])); + msg(wkwebview, s("loadRequest:"), msg(c("NSURLRequest"), s("requestWithURL:"), html)); - // Add code that sets up the initial state, EG: State Stores. - temp = concat(internalCode, getInitialState(app)); - free((void*)internalCode); - internalCode = temp; + Debug(app, "Loading Internal Code"); + // 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); - // Loop over assets and build up one giant Mother Of All Evals - int index = 1; - while(1) { - // Get next asset pointer - const unsigned char *asset = assets[index]; + // Add code that sets up the initial state, EG: State Stores. + temp = concat(internalCode, getInitialState(app)); + free((void*)internalCode); + internalCode = temp; - // If we have no more assets, break - if (asset == 0x00) { - break; - } + // Loop over assets and build up one giant Mother Of All Evals + int index = 1; + while(1) { + // Get next asset pointer + const unsigned char *asset = assets[index]; - temp = concat(internalCode, (const char *)asset); - free((void*)internalCode); - internalCode = temp; - index++; - }; + // If we have no more assets, break + if (asset == 0x00) { + break; + } - // Disable context menu if not in debug mode - if( debug != 1 ) { - temp = concat(internalCode, "wails._.DisableContextMenu();"); - free((void*)internalCode); - internalCode = temp; - } + temp = concat(internalCode, (const char *)asset); + free((void*)internalCode); + internalCode = temp; + index++; + }; - // class_addMethod(delegateClass, s("applicationWillFinishLaunching:"), (IMP) willFinishLaunching, "@@:@"); - // Include callback after evaluation - temp = concat(internalCode, "webkit.messageHandlers.completed.postMessage(true);"); - free((void*)internalCode); - internalCode = temp; + // Disable context menu if not in debug mode + if( debug != 1 ) { + temp = concat(internalCode, "wails._.DisableContextMenu();"); + free((void*)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);"; - // ExecJS(app, viewportScriptString); + // class_addMethod(delegateClass, s("applicationWillFinishLaunching:"), (IMP) willFinishLaunching, "@@:@"); + // Include callback after evaluation + temp = concat(internalCode, "webkit.messageHandlers.completed.postMessage(true);"); + free((void*)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);"; + // ExecJS(app, viewportScriptString); - // This evaluates the MOAE once the Dom has finished loading - msg(manager, - s("addUserScript:"), - msg(msg(c("WKUserScript"), s("alloc")), - s("initWithSource:injectionTime:forMainFrameOnly:"), - str(internalCode), - 1, - 1)); + // This evaluates the MOAE once the Dom has finished loading + msg(manager, + s("addUserScript:"), + msg(msg(c("WKUserScript"), s("alloc")), + s("initWithSource:injectionTime:forMainFrameOnly:"), + str(internalCode), + 1, + 1)); - if( app->webviewIsTranparent == 1 ) { - msg(wkwebview, s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 0), str("drawsBackground")); - } + if( app->webviewIsTranparent == 1 ) { + msg(wkwebview, s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 0), str("drawsBackground")); + } - if( app->menuAsJSON != NULL ) { - parseMenuData(app); - } + if( app->menuAsJSON != NULL ) { + parseMenuData(app); + } - // Finally call run - Debug(app, "Run called"); - msg(app->application, s("run")); + // Finally call run + Debug(app, "Run called"); + msg(app->application, s("run")); - free((void*)internalCode); + free((void*)internalCode); } #endif diff --git a/v2/internal/ffenestri/ffenestri_darwin.go b/v2/internal/ffenestri/ffenestri_darwin.go index 72c449c9..dbca5017 100644 --- a/v2/internal/ffenestri/ffenestri_darwin.go +++ b/v2/internal/ffenestri/ffenestri_darwin.go @@ -19,8 +19,6 @@ extern void SetMenu(void *, const char *); import "C" import ( "encoding/json" - - "github.com/wailsapp/wails/v2/pkg/menu" ) func (a *Application) processPlatformSettings() error { @@ -106,80 +104,3 @@ func (a *Application) processPlatformSettings() error { return nil } - -// ProcessedMenu is the original menu with the addition -// of radio groups extracted from the menu data -type ProcessedMenu struct { - Menu *menu.Menu - RadioGroups []*RadioGroup - currentRadioGroup []string -} - -// RadioGroup holds all the members of the same radio group -type RadioGroup struct { - Members []string - Length int -} - -// NewProcessedMenu processed the given menu and returns -// the original menu with the extracted radio groups -func NewProcessedMenu(menu *menu.Menu) *ProcessedMenu { - result := &ProcessedMenu{ - Menu: menu, - RadioGroups: []*RadioGroup{}, - currentRadioGroup: []string{}, - } - - result.processMenu() - - return result -} - -func (p *ProcessedMenu) processMenu() { - // Loop over top level menus - for _, item := range p.Menu.Items { - // Process MenuItem - p.processMenuItem(item) - } - - p.finaliseRadioGroup() -} - -func (p *ProcessedMenu) processMenuItem(item *menu.MenuItem) { - - switch item.Type { - - // We need to recurse submenus - case menu.SubmenuType: - - // Finalise any current radio groups as they don't trickle down to submenus - p.finaliseRadioGroup() - - // Process each submenu item - for _, subitem := range item.SubMenu { - p.processMenuItem(subitem) - } - case menu.RadioType: - // Add the item to the radio group - p.currentRadioGroup = append(p.currentRadioGroup, item.ID) - default: - p.finaliseRadioGroup() - } -} - -func (p *ProcessedMenu) finaliseRadioGroup() { - - // If we were processing a radio group, fix up the references - if len(p.currentRadioGroup) > 0 { - - // Create new radiogroup - group := &RadioGroup{ - Members: p.currentRadioGroup, - Length: len(p.currentRadioGroup), - } - p.RadioGroups = append(p.RadioGroups, group) - - // Empty the radio group - p.currentRadioGroup = []string{} - } -} diff --git a/v2/internal/ffenestri/menu.go b/v2/internal/ffenestri/menu.go new file mode 100644 index 00000000..8216b0d6 --- /dev/null +++ b/v2/internal/ffenestri/menu.go @@ -0,0 +1,80 @@ +package ffenestri + +import "github.com/wailsapp/wails/v2/pkg/menu" + +// ProcessedMenu is the original menu with the addition +// of radio groups extracted from the menu data +type ProcessedMenu struct { + Menu *menu.Menu + RadioGroups []*RadioGroup + currentRadioGroup []string +} + +// RadioGroup holds all the members of the same radio group +type RadioGroup struct { + Members []string + Length int +} + +// NewProcessedMenu processed the given menu and returns +// the original menu with the extracted radio groups +func NewProcessedMenu(menu *menu.Menu) *ProcessedMenu { + result := &ProcessedMenu{ + Menu: menu, + RadioGroups: []*RadioGroup{}, + currentRadioGroup: []string{}, + } + + result.processMenu() + + return result +} + +func (p *ProcessedMenu) processMenu() { + // Loop over top level menus + for _, item := range p.Menu.Items { + // Process MenuItem + p.processMenuItem(item) + } + + p.finaliseRadioGroup() +} + +func (p *ProcessedMenu) processMenuItem(item *menu.MenuItem) { + + switch item.Type { + + // We need to recurse submenus + case menu.SubmenuType: + + // Finalise any current radio groups as they don't trickle down to submenus + p.finaliseRadioGroup() + + // Process each submenu item + for _, subitem := range item.SubMenu { + p.processMenuItem(subitem) + } + case menu.RadioType: + // Add the item to the radio group + p.currentRadioGroup = append(p.currentRadioGroup, item.ID) + default: + p.finaliseRadioGroup() + } +} + +func (p *ProcessedMenu) finaliseRadioGroup() { + + // If we were processing a radio group, fix up the references + if len(p.currentRadioGroup) > 0 { + + // Create new radiogroup + group := &RadioGroup{ + Members: p.currentRadioGroup, + Length: len(p.currentRadioGroup), + } + p.RadioGroups = append(p.RadioGroups, group) + + // Empty the radio group + p.currentRadioGroup = []string{} + } +} diff --git a/v2/internal/messagedispatcher/dispatchclient.go b/v2/internal/messagedispatcher/dispatchclient.go index dad9dd7e..f8131bfa 100644 --- a/v2/internal/messagedispatcher/dispatchclient.go +++ b/v2/internal/messagedispatcher/dispatchclient.go @@ -2,6 +2,7 @@ package messagedispatcher import ( "fmt" + "github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/internal/logger" "github.com/wailsapp/wails/v2/internal/messagedispatcher/message" @@ -30,6 +31,7 @@ type Client interface { WindowUnFullscreen() WindowSetColour(colour int) DarkModeEnabled(callbackID string) + UpdateMenu(menu *menu.Menu) } // DispatchClient is what the frontends use to interface with the @@ -73,14 +75,6 @@ func (d *DispatchClient) DispatchMessage(incomingMessage string) { d.logger.Trace("I got a parsedMessage: %+v", parsedMessage) - // Check error - if err != nil { - d.logger.Error(err.Error()) - // Hrm... what do we do with this? - d.bus.PublishForTarget("generic:message", incomingMessage, d.id) - return - } - // Publish the parsed message d.bus.PublishForTarget(parsedMessage.Topic, parsedMessage.Data, d.id) diff --git a/v2/internal/messagedispatcher/messagedispatcher.go b/v2/internal/messagedispatcher/messagedispatcher.go index 744d3dbd..2a2a610c 100644 --- a/v2/internal/messagedispatcher/messagedispatcher.go +++ b/v2/internal/messagedispatcher/messagedispatcher.go @@ -2,6 +2,7 @@ package messagedispatcher import ( "encoding/json" + "github.com/wailsapp/wails/v2/pkg/menu" "strconv" "strings" "sync" @@ -22,6 +23,7 @@ type Dispatcher struct { windowChannel <-chan *servicebus.Message dialogChannel <-chan *servicebus.Message systemChannel <-chan *servicebus.Message + menuChannel <-chan *servicebus.Message running bool servicebus *servicebus.ServiceBus @@ -69,6 +71,11 @@ func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher, return nil, err } + menuChannel, err := servicebus.Subscribe("menu:") + if err != nil { + return nil, err + } + result := &Dispatcher{ servicebus: servicebus, eventChannel: eventChannel, @@ -79,6 +86,7 @@ func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher, windowChannel: windowChannel, dialogChannel: dialogChannel, systemChannel: systemChannel, + menuChannel: menuChannel, } return result, nil @@ -108,6 +116,8 @@ func (d *Dispatcher) Start() error { d.processDialogMessage(dialogMessage) case systemMessage := <-d.systemChannel: d.processSystemMessage(systemMessage) + case menuMessage := <-d.menuChannel: + d.processMenuMessage(menuMessage) } } @@ -177,6 +187,7 @@ func (d *Dispatcher) processCallResult(result *servicebus.Message) { if client == nil { // This is fatal - unknown target! d.logger.Fatal("Unknown target for call result: %+v", result) + return } d.logger.Trace("Sending message to client %s: R%s", target, result.Data().(string)) @@ -397,3 +408,31 @@ func (d *Dispatcher) processDialogMessage(result *servicebus.Message) { } } + +func (d *Dispatcher) processMenuMessage(result *servicebus.Message) { + splitTopic := strings.Split(result.Topic(), ":") + if len(splitTopic) < 2 { + d.logger.Error("Invalid menu message : %#v", result.Data()) + return + } + + command := splitTopic[1] + switch command { + case "update": + + updatedMenu, ok := result.Data().(*menu.Menu) + if !ok { + d.logger.Error("Invalid data for 'dialog:select:open' : %#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.UpdateMenu(updatedMenu) + } + + default: + d.logger.Error("Unknown dialog command: %s", command) + } +} diff --git a/v2/internal/runtime/menu.go b/v2/internal/runtime/menu.go index c4eabdc5..f5b53da8 100644 --- a/v2/internal/runtime/menu.go +++ b/v2/internal/runtime/menu.go @@ -9,16 +9,19 @@ import ( // Menu defines all Menu related operations type Menu interface { On(menuID string, callback func(*menu.MenuItem)) + Update() } type menuRuntime struct { - bus *servicebus.ServiceBus + bus *servicebus.ServiceBus + menu *menu.Menu } // newMenu creates a new Menu struct -func newMenu(bus *servicebus.ServiceBus) Menu { +func newMenu(bus *servicebus.ServiceBus, menu *menu.Menu) Menu { return &menuRuntime{ - bus: bus, + bus: bus, + menu: menu, } } @@ -29,3 +32,7 @@ func (m *menuRuntime) On(menuID string, callback func(*menu.MenuItem)) { Callback: callback, }) } + +func (m *menuRuntime) Update() { + m.bus.Publish("menu:update", m.menu) +} diff --git a/v2/internal/runtime/runtime.go b/v2/internal/runtime/runtime.go index a69b7165..f518cb4e 100644 --- a/v2/internal/runtime/runtime.go +++ b/v2/internal/runtime/runtime.go @@ -1,6 +1,9 @@ package runtime -import "github.com/wailsapp/wails/v2/internal/servicebus" +import ( + "github.com/wailsapp/wails/v2/internal/servicebus" + "github.com/wailsapp/wails/v2/pkg/menu" +) // Runtime is a means for the user to interact with the application at runtime type Runtime struct { @@ -16,14 +19,14 @@ type Runtime struct { } // New creates a new runtime -func New(serviceBus *servicebus.ServiceBus) *Runtime { +func New(serviceBus *servicebus.ServiceBus, menu *menu.Menu) *Runtime { result := &Runtime{ Browser: newBrowser(), Events: newEvents(serviceBus), Window: newWindow(serviceBus), Dialog: newDialog(serviceBus), System: newSystem(serviceBus), - Menu: newMenu(serviceBus), + Menu: newMenu(serviceBus, menu), Log: newLog(serviceBus), bus: serviceBus, } diff --git a/v2/internal/subsystem/menu.go b/v2/internal/subsystem/menu.go index a651176d..bba5d400 100644 --- a/v2/internal/subsystem/menu.go +++ b/v2/internal/subsystem/menu.go @@ -34,10 +34,13 @@ type Menu struct { // logger logger logger.CustomLogger + + // The application menu + applicationMenu *menu.Menu } // NewMenu creates a new menu subsystem -func NewMenu(initialMenu *menu.Menu, bus *servicebus.ServiceBus, logger *logger.Logger) (*Menu, error) { +func NewMenu(applicationMenu *menu.Menu, bus *servicebus.ServiceBus, logger *logger.Logger) (*Menu, error) { // Register quit channel quitChannel, err := bus.Subscribe("quit") @@ -52,15 +55,16 @@ func NewMenu(initialMenu *menu.Menu, bus *servicebus.ServiceBus, logger *logger. } result := &Menu{ - quitChannel: quitChannel, - menuChannel: menuChannel, - logger: logger.CustomLogger("Menu Subsystem"), - listeners: make(map[string][]func(*menu.MenuItem)), - menuItems: make(map[string]*menu.MenuItem), + quitChannel: quitChannel, + menuChannel: menuChannel, + logger: logger.CustomLogger("Menu Subsystem"), + listeners: make(map[string][]func(*menu.MenuItem)), + menuItems: make(map[string]*menu.MenuItem), + applicationMenu: applicationMenu, } // Build up list of item/id pairs - result.processMenu(initialMenu) + result.processMenu(applicationMenu) return result, nil } diff --git a/v2/internal/subsystem/runtime.go b/v2/internal/subsystem/runtime.go index 1f3a5745..0baa42e5 100644 --- a/v2/internal/subsystem/runtime.go +++ b/v2/internal/subsystem/runtime.go @@ -2,6 +2,7 @@ package subsystem import ( "fmt" + "github.com/wailsapp/wails/v2/pkg/menu" "strings" "github.com/wailsapp/wails/v2/internal/logger" @@ -23,7 +24,7 @@ type Runtime struct { } // NewRuntime creates a new runtime subsystem -func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger) (*Runtime, error) { +func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, menu *menu.Menu) (*Runtime, error) { // Register quit channel quitChannel, err := bus.Subscribe("quit") @@ -41,7 +42,7 @@ func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger) (*Runtime, er quitChannel: quitChannel, runtimeChannel: runtimeChannel, logger: logger.CustomLogger("Runtime Subsystem"), - runtime: runtime.New(bus), + runtime: runtime.New(bus, menu), } return result, nil @@ -75,7 +76,8 @@ func (r *Runtime) Start() error { case "browser": err = r.processBrowserMessage(method, runtimeMessage.Data()) default: - fmt.Errorf("unknown runtime message: %+v", runtimeMessage) + err = fmt.Errorf("unknown runtime message: %+v", + runtimeMessage) } // If we had an error, log it diff --git a/v2/pkg/menu/menuitem.go b/v2/pkg/menu/menuitem.go index 0fb4f12e..59e07d46 100644 --- a/v2/pkg/menu/menuitem.go +++ b/v2/pkg/menu/menuitem.go @@ -20,6 +20,27 @@ type MenuItem struct { Checked bool // Submenu contains a list of menu items that will be shown as a submenu SubMenu []*MenuItem `json:"SubMenu,omitempty"` + + // This holds the menu item's parent. + parent *MenuItem +} + +// Parent returns the parent of the menu item. +// If it is a top level menu then it returns nil. +func (m *MenuItem) Parent() *MenuItem { + return m.parent +} + +// Append will attempt to append the given menu item to +// this item's submenu items. If this menu item is not a +// submenu, then this method will not add the item and +// simply return false. +func (m *MenuItem) Append(item *MenuItem) bool { + if m.Type != SubmenuType { + return false + } + m.SubMenu = append(m.SubMenu, item) + return true } // Text is a helper to create basic Text menu items @@ -78,9 +99,16 @@ func CheckboxWithAccelerator(label string, id string, checked bool, accelerator // SubMenu is a helper to create Submenus func SubMenu(label string, items []*MenuItem) *MenuItem { - return &MenuItem{ + result := &MenuItem{ Label: label, SubMenu: items, Type: SubmenuType, } + + // Fix up parent pointers + for _, item := range items { + item.parent = result + } + + return result } diff --git a/v2/test/kitchensink/menu.go b/v2/test/kitchensink/menu.go index db346349..c5384869 100644 --- a/v2/test/kitchensink/menu.go +++ b/v2/test/kitchensink/menu.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "strconv" wails "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/menu" @@ -10,6 +11,8 @@ import ( // Menu struct type Menu struct { runtime *wails.Runtime + + dynamicMenuCounter int } // WailsInit is called at application startup @@ -18,21 +21,32 @@ func (m *Menu) WailsInit(runtime *wails.Runtime) error { m.runtime = runtime // Setup Menu Listeners - m.runtime.Menu.On("hello", func(m *menu.MenuItem) { - fmt.Printf("The '%s' menu was clicked\n", m.Label) + m.runtime.Menu.On("hello", func(mi *menu.MenuItem) { + fmt.Printf("The '%s' menu was clicked\n", mi.Label) }) - m.runtime.Menu.On("checkbox-menu", func(m *menu.MenuItem) { - fmt.Printf("The '%s' menu was clicked\n", m.Label) - fmt.Printf("It is now %v\n", m.Checked) - // m.Checked = false - // runtime.Menu.Update() + m.runtime.Menu.On("checkbox-menu", func(mi *menu.MenuItem) { + fmt.Printf("The '%s' menu was clicked\n", mi.Label) + fmt.Printf("It is now %v\n", mi.Checked) }) - m.runtime.Menu.On("😀option-1", func(m *menu.MenuItem) { - fmt.Printf("We can use UTF-8 IDs: %s\n", m.Label) + m.runtime.Menu.On("😀option-1", func(mi *menu.MenuItem) { + fmt.Printf("We can use UTF-8 IDs: %s\n", mi.Label) }) + + // Setup dynamic menus + m.runtime.Menu.On("Add Menu Item", m.addMenu) return nil } +func (m *Menu) addMenu(mi *menu.MenuItem) { + // Get this menu's parent + parent := mi.Parent() + m.dynamicMenuCounter++ + menuText := "Dynamic Menu Item " + strconv.Itoa(m.dynamicMenuCounter) + parent.Append(menu.Text(menuText, menuText)) + // parent.Append(menu.TextWithAccelerator(menuText, menuText, menu.Accel("["))) + m.runtime.Menu.Update() +} + func createApplicationMenu() *menu.Menu { // Create menu @@ -54,6 +68,7 @@ func createApplicationMenu() *menu.Menu { menu.Front(), menu.SubMenu("Test Submenu", []*menu.MenuItem{ + menu.Text("Plain text", "plain text"), menu.SubMenu("Accelerators", []*menu.MenuItem{ menu.SubMenu("Modifiers", []*menu.MenuItem{ menu.TextWithAccelerator("Shift accelerator", "Shift", menu.ShiftAccel("o")), @@ -76,9 +91,6 @@ func createApplicationMenu() *menu.Menu { menu.TextWithAccelerator("End", "End", menu.Accel("End")), menu.TextWithAccelerator("Page Up", "Page Up", menu.Accel("Page Up")), menu.TextWithAccelerator("Page Down", "Page Down", menu.Accel("Page Down")), - menu.TextWithAccelerator("Insert", "Insert", menu.Accel("Insert")), - menu.TextWithAccelerator("PrintScreen", "PrintScreen", menu.Accel("PrintScreen")), - menu.TextWithAccelerator("ScrollLock", "ScrollLock", menu.Accel("ScrollLock")), menu.TextWithAccelerator("NumLock", "NumLock", menu.Accel("NumLock")), }), menu.SubMenu("Function Keys", []*menu.MenuItem{ @@ -102,39 +114,28 @@ func createApplicationMenu() *menu.Menu { menu.TextWithAccelerator("F18", "F18", menu.Accel("F18")), menu.TextWithAccelerator("F19", "F19", menu.Accel("F19")), menu.TextWithAccelerator("F20", "F20", menu.Accel("F20")), - menu.TextWithAccelerator("F21", "F21", menu.Accel("F21")), - menu.TextWithAccelerator("F22", "F22", menu.Accel("F22")), - menu.TextWithAccelerator("F23", "F23", menu.Accel("F23")), - menu.TextWithAccelerator("F24", "F24", menu.Accel("F24")), - menu.TextWithAccelerator("F25", "F25", menu.Accel("F25")), - menu.TextWithAccelerator("F26", "F26", menu.Accel("F26")), - menu.TextWithAccelerator("F27", "F27", menu.Accel("F27")), - menu.TextWithAccelerator("F28", "F28", menu.Accel("F28")), - menu.TextWithAccelerator("F29", "F29", menu.Accel("F29")), - menu.TextWithAccelerator("F30", "F30", menu.Accel("F30")), - menu.TextWithAccelerator("F31", "F31", menu.Accel("F31")), - menu.TextWithAccelerator("F32", "F32", menu.Accel("F32")), - menu.TextWithAccelerator("F33", "F33", menu.Accel("F33")), - menu.TextWithAccelerator("F34", "F34", menu.Accel("F34")), - menu.TextWithAccelerator("F35", "F35", menu.Accel("F35")), }), menu.SubMenu("Standard Keys", []*menu.MenuItem{ menu.TextWithAccelerator("Backtick", "Backtick", menu.Accel("`")), menu.TextWithAccelerator("Plus", "Plus", menu.Accel("+")), }), + menu.SubMenu("Dynamic Menus", []*menu.MenuItem{ + menu.TextWithAccelerator("Add Menu Item", "Add Menu Item", menu.CmdOrCtrlAccel("+")), + menu.Separator(), + }), }), - &menu.MenuItem{ + { Label: "Disabled Menu", Type: menu.TextType, Accelerator: menu.ComboAccel("p", menu.CmdOrCtrl, menu.Shift), Disabled: true, }, - &menu.MenuItem{ + { Label: "Hidden Menu", Type: menu.TextType, Hidden: true, }, - &menu.MenuItem{ + { ID: "checkbox-menu", Label: "Checkbox Menu", Type: menu.CheckboxType,