From 3f82ceabbacedc32f41d62579ac63e9b013789da Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sat, 30 Apr 2022 12:00:00 +1000 Subject: [PATCH] Initial Tray support for Mac --- .gitignore | 1 + .../frontend/desktop/darwin/AppDelegate.m | 2 + .../frontend/desktop/darwin/Application.h | 4 + .../frontend/desktop/darwin/Application.m | 15 ++++ .../frontend/desktop/darwin/frontend.go | 78 +++++++++++++++---- v2/internal/frontend/desktop/darwin/menu.go | 14 ++++ v2/internal/frontend/desktop/darwin/message.h | 1 + .../frontend/desktop/darwin/traymenu.go | 36 +++++++++ v2/internal/frontend/desktop/darwin/window.go | 4 + v2/internal/frontend/frontend.go | 3 + v2/pkg/menu/tray.go | 31 +++++++- v2/pkg/options/options.go | 7 +- 12 files changed, 178 insertions(+), 18 deletions(-) create mode 100644 v2/internal/frontend/desktop/darwin/traymenu.go diff --git a/.gitignore b/.gitignore index 32e4ec11e..d621a4358 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ v2/test/kitchensink/frontend/package.json.md5 !v2/internal/ffenestri/windows/x64/WebView2Loader.dll .idea/ v2/cmd/wails/internal/commands/initialise/templates/testtemplates/ +v2/internal/frontend/desktop/darwin/test.xcodeproj \ No newline at end of file diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.m b/v2/internal/frontend/desktop/darwin/AppDelegate.m index 6d46deae4..c985fb9b6 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.m +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.m @@ -9,6 +9,7 @@ #import #import "AppDelegate.h" +#import "message.h" @implementation AppDelegate - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { @@ -25,6 +26,7 @@ } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + processNotification(0); // Notify Go [NSApp activateIgnoringOtherApps:YES]; if ( self.startFullscreen ) { NSWindowCollectionBehavior behaviour = [self.mainWindow collectionBehavior]; diff --git a/v2/internal/frontend/desktop/darwin/Application.h b/v2/internal/frontend/desktop/darwin/Application.h index 41423965a..363461107 100644 --- a/v2/internal/frontend/desktop/darwin/Application.h +++ b/v2/internal/frontend/desktop/darwin/Application.h @@ -57,6 +57,10 @@ void AppendRole(void *inctx, void *inMenu, int role); void SetAsApplicationMenu(void *inctx, void *inMenu); void UpdateApplicationMenu(void *inctx); +/* Tray Menu */ +void* NewNSStatusItem(const char* label); +void SetTrayMenu(void *nsStatusItem, void* nsMenu); + void SetAbout(void *inctx, const char* title, const char* description, void* imagedata, int datalen); void* AppendMenuItem(void* inctx, void* nsmenu, const char* label, const char* shortcutKey, int modifiers, int disabled, int checked, int menuItemID); void AppendSeparator(void* inMenu); diff --git a/v2/internal/frontend/desktop/darwin/Application.m b/v2/internal/frontend/desktop/darwin/Application.m index da1ffac16..2f82c5aad 100644 --- a/v2/internal/frontend/desktop/darwin/Application.m +++ b/v2/internal/frontend/desktop/darwin/Application.m @@ -270,6 +270,21 @@ void AppendRole(void *inctx, void *inMenu, int role) { [menu appendRole :ctx :role]; } +void* NewNSStatusItem(const char* label) { + NSString *_label = safeInit(label); + NSStatusBar *statusBar = [NSStatusBar systemStatusBar]; + NSStatusItem *result = [[statusBar statusItemWithLength:NSVariableStatusItemLength] retain]; + [result button].title = _label; + return result; +} + +void SetTrayMenu(void *nsStatusItem, void* nsMenu) { + ON_MAIN_THREAD( + [(NSStatusItem*)nsStatusItem setMenu:(NSMenu *)nsMenu]; + ) +} + + void* NewMenu(const char *name) { NSString *title = @""; if (name != nil) { diff --git a/v2/internal/frontend/desktop/darwin/frontend.go b/v2/internal/frontend/desktop/darwin/frontend.go index 1d0dbb75d..492d42b32 100644 --- a/v2/internal/frontend/desktop/darwin/frontend.go +++ b/v2/internal/frontend/desktop/darwin/frontend.go @@ -3,15 +3,6 @@ package darwin -/* -#cgo CFLAGS: -x objective-c -#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit -#import -#import "Application.h" -#import "WailsContext.h" - -#include -*/ import "C" import ( "bytes" @@ -31,14 +22,33 @@ import ( "github.com/wailsapp/wails/v2/internal/frontend" "github.com/wailsapp/wails/v2/internal/frontend/assetserver" "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/options" ) +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit +#import +#import "Application.h" +#import "WailsContext.h" + +#include +*/ +import "C" + const startURL = "wails://wails/" +type NotificationType uint8 + +const ( + ApplicationDidFinishLaunching NotificationType = 0 +) + var messageBuffer = make(chan string, 100) var requestBuffer = make(chan *request, 100) var callbackBuffer = make(chan uint, 10) +var notificationBuffer = make(chan NotificationType, 10) type Frontend struct { @@ -57,15 +67,22 @@ type Frontend struct { mainWindow *Window bindings *binding.Bindings dispatcher frontend.Dispatcher + trayMenus map[*menu.TrayMenu]*NSTrayMenu + + applicationDidFinishLaunching bool + notificationCallbacks map[NotificationType][]func() + trayMenusBuffer []*menu.TrayMenu } func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend { result := &Frontend{ - frontendOptions: appoptions, - logger: myLogger, - bindings: appBindings, - dispatcher: dispatcher, - ctx: ctx, + frontendOptions: appoptions, + logger: myLogger, + bindings: appBindings, + dispatcher: dispatcher, + ctx: ctx, + trayMenus: make(map[*menu.TrayMenu]*NSTrayMenu), + notificationCallbacks: make(map[NotificationType][]func()), } result.startURL, _ = url.Parse(startURL) @@ -88,10 +105,17 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. go result.startMessageProcessor() go result.startCallbackProcessor() + go result.startNotificationsProcessor() + + result.registerNotificationCallback(ApplicationDidFinishLaunching, result.processTrayMenus) return result } +func (f *Frontend) registerNotificationCallback(notificationType NotificationType, callback func()) { + f.notificationCallbacks[notificationType] = append(f.notificationCallbacks[notificationType], callback) +} + func (f *Frontend) startMessageProcessor() { for message := range messageBuffer { f.processMessage(message) @@ -110,6 +134,11 @@ func (f *Frontend) startCallbackProcessor() { } } } +func (f *Frontend) startNotificationsProcessor() { + for notification := range notificationBuffer { + f.handleNotification(notification) + } +} func (f *Frontend) WindowReload() { f.ExecJS("runtime.WindowReload();") @@ -393,6 +422,11 @@ func processMessage(message *C.char) { messageBuffer <- goMessage } +//export processNotification +func processNotification(notification NotificationType) { + notificationBuffer <- notification +} + //export processURLRequest func processURLRequest(ctx unsafe.Pointer, url *C.char, method *C.char, headers *C.char, body unsafe.Pointer, bodyLen C.int) { var goBody []byte @@ -413,3 +447,19 @@ func processURLRequest(ctx unsafe.Pointer, url *C.char, method *C.char, headers func processCallback(callbackID uint) { callbackBuffer <- callbackID } + +func (f *Frontend) handleNotification(notification NotificationType) { + switch notification { + case ApplicationDidFinishLaunching: + f.applicationDidFinishLaunching = true + for _, callback := range f.notificationCallbacks[notification] { + go callback() + } + } +} + +func (f *Frontend) processTrayMenus() { + for _, trayMenu := range f.trayMenusBuffer { + f.mainWindow.TrayMenuAdd(trayMenu) + } +} diff --git a/v2/internal/frontend/desktop/darwin/menu.go b/v2/internal/frontend/desktop/darwin/menu.go index 8afb63fb9..65449f7f2 100644 --- a/v2/internal/frontend/desktop/darwin/menu.go +++ b/v2/internal/frontend/desktop/darwin/menu.go @@ -20,6 +20,20 @@ import ( "github.com/wailsapp/wails/v2/pkg/menu/keys" ) +func NewNSTrayMenu(context unsafe.Pointer, trayMenu *menu.TrayMenu) *NSTrayMenu { + c := NewCalloc() + defer c.Free() + theMenu := NewNSMenu(context, "") + processMenu(theMenu, trayMenu.Menu) + title := c.String(trayMenu.Label) + nsStatusItem := C.NewNSStatusItem(title) + C.SetTrayMenu(nsStatusItem, theMenu.nsmenu) + return &NSTrayMenu{ + context: context, + nsStatusItem: nsStatusItem, + } +} + type NSMenu struct { context unsafe.Pointer nsmenu unsafe.Pointer diff --git a/v2/internal/frontend/desktop/darwin/message.h b/v2/internal/frontend/desktop/darwin/message.h index f0a5f482b..8ea2bda3e 100644 --- a/v2/internal/frontend/desktop/darwin/message.h +++ b/v2/internal/frontend/desktop/darwin/message.h @@ -20,6 +20,7 @@ void processMessageDialogResponse(int); void processOpenFileDialogResponse(const char*); void processSaveFileDialogResponse(const char*); void processCallback(int); +void processNotification(int); #ifdef __cplusplus } diff --git a/v2/internal/frontend/desktop/darwin/traymenu.go b/v2/internal/frontend/desktop/darwin/traymenu.go new file mode 100644 index 000000000..3b214edf8 --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/traymenu.go @@ -0,0 +1,36 @@ +package darwin + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit +#import +#import "Application.h" +#import "WailsContext.h" + +#include +*/ + +import "C" +import ( + "unsafe" + + "github.com/wailsapp/wails/v2/pkg/menu" +) + +func (f *Frontend) TrayMenuAdd(trayMenu *menu.TrayMenu) { + if f.applicationDidFinishLaunching == false { + f.trayMenusBuffer = append(f.trayMenusBuffer, trayMenu) + return + } + nsTrayMenu := f.mainWindow.TrayMenuAdd(trayMenu) + f.trayMenus[trayMenu] = nsTrayMenu +} + +type NSTrayMenu struct { + context unsafe.Pointer + nsStatusItem unsafe.Pointer // NSStatusItem +} + +func (w *Window) TrayMenuAdd(trayMenu *menu.TrayMenu) *NSTrayMenu { + return NewNSTrayMenu(w.context, trayMenu) +} diff --git a/v2/internal/frontend/desktop/darwin/window.go b/v2/internal/frontend/desktop/darwin/window.go index ec6e20d46..2437b5126 100644 --- a/v2/internal/frontend/desktop/darwin/window.go +++ b/v2/internal/frontend/desktop/darwin/window.go @@ -112,6 +112,10 @@ func NewWindow(frontendOptions *options.App, debugMode bool) *Window { result.SetApplicationMenu(frontendOptions.Menu) } + if frontendOptions.TrayMenu != nil { + result.TrayMenuAdd(frontendOptions.TrayMenu) + } + return result } diff --git a/v2/internal/frontend/frontend.go b/v2/internal/frontend/frontend.go index 6dd12c6b0..473712198 100644 --- a/v2/internal/frontend/frontend.go +++ b/v2/internal/frontend/frontend.go @@ -101,4 +101,7 @@ type Frontend interface { // Browser BrowserOpenURL(url string) + + // Tray Menu + TrayMenuAdd(trayMenu *menu.TrayMenu) } diff --git a/v2/pkg/menu/tray.go b/v2/pkg/menu/tray.go index 7554795ad..87cca6629 100644 --- a/v2/pkg/menu/tray.go +++ b/v2/pkg/menu/tray.go @@ -1,7 +1,18 @@ package menu +import ( + "context" + "log" + goruntime "runtime" +) + +type TrayMenuAdd interface { + TrayMenuAdd(menu *TrayMenu) +} + // TrayMenu are the options type TrayMenu struct { + ctx context.Context // Label is the text we wish to display in the tray Label string @@ -27,7 +38,7 @@ type TrayMenu struct { Tooltip string // Callback function when menu clicked - //Click Callback `json:"-"` + Click Callback // Disabled makes the item unselectable Disabled bool @@ -41,3 +52,21 @@ type TrayMenu struct { // OnClose is called when the Menu is closed OnClose func() } + +func NewTrayMenu(ctx context.Context) *TrayMenu { + return &TrayMenu{ + ctx: ctx, + } +} + +func (t *TrayMenu) Show() { + result := t.ctx.Value("frontend") + if result == nil { + pc, _, _, _ := goruntime.Caller(1) + funcName := goruntime.FuncForPC(pc).Name() + log.Fatalf("invalid context at '%s'", funcName) + } + println("\n\n\n\nFWEFWEFWFE") + result.(TrayMenuAdd).TrayMenuAdd(t) + +} diff --git a/v2/pkg/options/options.go b/v2/pkg/options/options.go index 1e0b9dfca..e75793903 100644 --- a/v2/pkg/options/options.go +++ b/v2/pkg/options/options.go @@ -56,9 +56,10 @@ type App struct { //ContextMenus []*menu.ContextMenu //TrayMenus []*menu.TrayMenu - Windows *windows.Options - Mac *mac.Options - Linux *linux.Options + Windows *windows.Options + Mac *mac.Options + Linux *linux.Options + TrayMenu *menu.TrayMenu } type RGBA struct {