From 16ce9e562f367f967ee0b97c55bdae0e957611ea Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Mon, 3 Jul 2023 20:15:38 +1000 Subject: [PATCH] [v3 mac] Window alignment to systray --- v3/examples/systray/main.go | 9 +- v3/pkg/application/screen_darwin.go | 30 +++++ v3/pkg/application/systemtray_darwin.go | 150 ++++++++++-------------- v3/pkg/application/systemtray_darwin.h | 15 +++ v3/pkg/application/systemtray_darwin.m | 108 +++++++++++++++++ 5 files changed, 219 insertions(+), 93 deletions(-) create mode 100644 v3/pkg/application/systemtray_darwin.h create mode 100644 v3/pkg/application/systemtray_darwin.m diff --git a/v3/examples/systray/main.go b/v3/examples/systray/main.go index bbb80255f..a5e1e22a8 100644 --- a/v3/examples/systray/main.go +++ b/v3/examples/systray/main.go @@ -68,9 +68,11 @@ func main() { app.Quit() }) - systemTray.SetMenu(myMenu) + if runtime.GOOS != "darwin" { + systemTray.SetMenu(myMenu) + } - systemTray.OnClick(func() { + showWindow := func() { window.SetTitle(fmt.Sprintf("Clicked %d times", clickCount())) err := systemTray.PositionWindow(window) if err != nil { @@ -78,7 +80,8 @@ func main() { return } window.Show().Focus() - }) + } + systemTray.OnClick(showWindow) err := app.Run() diff --git a/v3/pkg/application/screen_darwin.go b/v3/pkg/application/screen_darwin.go index 0669d531a..d35cf4611 100644 --- a/v3/pkg/application/screen_darwin.go +++ b/v3/pkg/application/screen_darwin.go @@ -97,6 +97,28 @@ Screen getScreenForWindow(void* window){ return processScreen(screen); } +// Get the screen for the system tray +Screen getScreenForSystemTray(void* nsStatusItem) { + NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; + NSRect frame = statusItem.button.frame; + NSArray *screens = NSScreen.screens; + NSScreen *associatedScreen = nil; + + for (NSScreen *screen in screens) { + if (NSPointInRect(frame.origin, screen.frame)) { + associatedScreen = screen; + break; + } + } + return processScreen(associatedScreen); +} + +void* getWindowForSystray(void* nsStatusItem) { + NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; + return statusItem.button.window; +} + + */ import "C" import "unsafe" @@ -149,3 +171,11 @@ func getScreenForWindow(window *macosWebviewWindow) (*Screen, error) { cScreen := C.getScreenForWindow(window.nsWindow) return cScreenToScreen(cScreen), nil } + +func getScreenForSystray(systray *macosSystemTray) (*Screen, error) { + // Get the Window for the status item + // https://stackoverflow.com/a/5875019/4188138 + window := C.getWindowForSystray(systray.nsStatusItem) + cScreen := C.getScreenForWindow(window) + return cScreenToScreen(cScreen), nil +} diff --git a/v3/pkg/application/systemtray_darwin.go b/v3/pkg/application/systemtray_darwin.go index 26f8d81d9..910291b90 100644 --- a/v3/pkg/application/systemtray_darwin.go +++ b/v3/pkg/application/systemtray_darwin.go @@ -8,91 +8,7 @@ package application #include "Cocoa/Cocoa.h" #include "menuitem_darwin.h" - -// Create a new system tray -void* systemTrayNew() { - NSStatusItem *statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength] retain]; - return (void*)statusItem; -} - -void systemTraySetLabel(void* nsStatusItem, char *label) { - if( label == NULL ) { - return; - } - // Set the label on the main thread - dispatch_async(dispatch_get_main_queue(), ^{ - NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; - statusItem.button.title = [NSString stringWithUTF8String:label]; - free(label); - }); -} - -// Create an nsimage from a byte array -NSImage* imageFromBytes(const unsigned char *bytes, int length) { - NSData *data = [NSData dataWithBytes:bytes length:length]; - NSImage *image = [[NSImage alloc] initWithData:data]; - return image; -} - -// Set the icon on the system tray -void systemTraySetIcon(void* nsStatusItem, void* nsImage, int position, bool isTemplate) { - // Set the icon on the main thread - dispatch_async(dispatch_get_main_queue(), ^{ - NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; - NSImage *image = (NSImage *)nsImage; - - NSStatusBar *statusBar = [NSStatusBar systemStatusBar]; - CGFloat thickness = [statusBar thickness]; - [image setSize:NSMakeSize(thickness, thickness)]; - if( isTemplate ) { - [image setTemplate:YES]; - } - statusItem.button.image = [image autorelease]; - statusItem.button.imagePosition = position; - }); -} - -// Add menu to system tray -void systemTraySetMenu(void* nsStatusItem, void* nsMenu) { - // Set the menu on the main thread - dispatch_async(dispatch_get_main_queue(), ^{ - NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; - NSMenu *menu = (NSMenu *)nsMenu; - statusItem.menu = menu; - }); -} - -// Destroy system tray -void systemTrayDestroy(void* nsStatusItem) { - // Remove the status item from the status bar and its associated menu - dispatch_async(dispatch_get_main_queue(), ^{ - NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; - [[NSStatusBar systemStatusBar] removeStatusItem:statusItem]; - [statusItem release]; - }); -} - -void systemTrayGetBounds(void* nsStatusItem, NSRect *rect) { - NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; - *rect = statusItem.button.window.frame; -} - -// Get the screen for the system tray -NSScreen* getScreenForSystemTray(void* nsStatusItem) { - NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; - NSRect frame = statusItem.button.frame; - NSArray *screens = NSScreen.screens; - NSScreen *associatedScreen = nil; - - for (NSScreen *screen in screens) { - if (NSPointInRect(frame.origin, screen.frame)) { - associatedScreen = screen; - break; - } - } - return associatedScreen; -} - +#include "systemtray_darwin.h" */ import "C" import ( @@ -110,6 +26,27 @@ type macosSystemTray struct { nsMenu unsafe.Pointer iconPosition int isTemplateIcon bool + parent *SystemTray +} + +type button int + +const ( + left button = iota + right +) + +// system tray map +var systemTrayMap = make(map[uint]*macosSystemTray) + +//export systrayClickCallback +func systrayClickCallback(id C.long, buttonID C.int) { + // Get the system tray + systemTray := systemTrayMap[uint(id)] + if systemTray == nil { + return + } + systemTray.processClick(button(buttonID)) } func (s *macosSystemTray) setIconPosition(position int) { @@ -192,13 +129,21 @@ func (s *macosSystemTray) positionWindow(window *WebviewWindow) error { } func (s *macosSystemTray) getScreen() (*Screen, error) { - cScreen := C.getScreenForSystemTray(s.nsStatusItem) - return cScreenToScreen(cScreen), nil + return getScreenForSystray(s) } func (s *macosSystemTray) bounds() (*Rect, error) { var rect C.NSRect - rect = C.systemTrayGetBounds(s.nsStatusItem) + C.systemTrayGetBounds(s.nsStatusItem, &rect) + // Get the screen height for the screen that the systray is on + screen, err := getScreenForSystray(s) + if err != nil { + return nil, err + } + + // Invert Y axis based on screen height + rect.origin.y = C.double(screen.Bounds.Height) - rect.origin.y - rect.size.height + return &Rect{ X: int(rect.origin.x), Y: int(rect.origin.y), @@ -212,7 +157,8 @@ func (s *macosSystemTray) run() { if s.nsStatusItem != nil { Fatal("System tray '%d' already running", s.id) } - s.nsStatusItem = unsafe.Pointer(C.systemTrayNew()) + s.nsStatusItem = unsafe.Pointer(C.systemTrayNew(C.long(s.id))) + if s.label != "" { C.systemTraySetLabel(s.nsStatusItem, C.CString(s.label)) } @@ -252,7 +198,8 @@ func (s *macosSystemTray) setTemplateIcon(icon []byte) { } func newSystemTrayImpl(s *SystemTray) systemTrayImpl { - return &macosSystemTray{ + result := &macosSystemTray{ + parent: s, id: s.id, label: s.label, icon: s.icon, @@ -260,6 +207,8 @@ func newSystemTrayImpl(s *SystemTray) systemTrayImpl { iconPosition: s.iconPosition, isTemplateIcon: s.isTemplateIcon, } + systemTrayMap[s.id] = result + return result } func (s *macosSystemTray) setLabel(label string) { @@ -271,3 +220,24 @@ func (s *macosSystemTray) destroy() { // Remove the status item from the status bar and its associated menu C.systemTrayDestroy(s.nsStatusItem) } + +func (s *macosSystemTray) processClick(b button) { + switch b { + case left: + // Check if we have a callback + if s.parent.clickHandler != nil { + s.parent.clickHandler() + return + } + case right: + // Check if we have a callback + if s.parent.rightClickHandler != nil { + s.parent.rightClickHandler() + return + } + } + // Open the default menu if we have one + if s.menu != nil { + C.showMenu(s.nsStatusItem) + } +} diff --git a/v3/pkg/application/systemtray_darwin.h b/v3/pkg/application/systemtray_darwin.h new file mode 100644 index 000000000..aa9f78a8c --- /dev/null +++ b/v3/pkg/application/systemtray_darwin.h @@ -0,0 +1,15 @@ +//go:build darwin + +@interface StatusItemController : NSObject +@property long id; +- (void)statusItemClicked:(id)sender; +@end + +void* systemTrayNew(long id); +void systemTraySetLabel(void* nsStatusItem, char *label); +NSImage* imageFromBytes(const unsigned char *bytes, int length); +void systemTraySetIcon(void* nsStatusItem, void* nsImage, int position, bool isTemplate); +void systemTraySetMenu(void* nsStatusItem, void* nsMenu); +void systemTrayDestroy(void* nsStatusItem); +void showMenu(void* nsStatusItem); +void systemTrayGetBounds(void* nsStatusItem, NSRect *rect); \ No newline at end of file diff --git a/v3/pkg/application/systemtray_darwin.m b/v3/pkg/application/systemtray_darwin.m new file mode 100644 index 000000000..7a8c7a5ea --- /dev/null +++ b/v3/pkg/application/systemtray_darwin.m @@ -0,0 +1,108 @@ +//go:build darwin + +#include "Cocoa/Cocoa.h" +#include "menuitem_darwin.h" +#include "systemtray_darwin.h" + +extern void systrayClickCallback(long, int); + +// StatusItemController.m +@implementation StatusItemController + +- (void)statusItemClicked:(id)sender { + // Get the left or right button + NSEvent *event = [NSApp currentEvent]; + if (event.type == NSEventTypeRightMouseUp) { + // Right click + systrayClickCallback(self.id, 1); + } else { + // Left click + systrayClickCallback(self.id, 0); + } +} + +@end + +// Create a new system tray +void* systemTrayNew(long id) { + StatusItemController *controller = [[StatusItemController alloc] init]; + controller.id = id; + NSStatusItem *statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength] retain]; + [statusItem setTarget:controller]; + [statusItem setAction:@selector(statusItemClicked:)]; + return (void*)statusItem; +} + +void systemTraySetLabel(void* nsStatusItem, char *label) { + if( label == NULL ) { + return; + } + // Set the label on the main thread + dispatch_async(dispatch_get_main_queue(), ^{ + NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; + statusItem.button.title = [NSString stringWithUTF8String:label]; + free(label); + }); +} + +// Create an nsimage from a byte array +NSImage* imageFromBytes(const unsigned char *bytes, int length) { + NSData *data = [NSData dataWithBytes:bytes length:length]; + NSImage *image = [[NSImage alloc] initWithData:data]; + return image; +} + +// Set the icon on the system tray +void systemTraySetIcon(void* nsStatusItem, void* nsImage, int position, bool isTemplate) { + // Set the icon on the main thread + dispatch_async(dispatch_get_main_queue(), ^{ + NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; + NSImage *image = (NSImage *)nsImage; + + NSStatusBar *statusBar = [NSStatusBar systemStatusBar]; + CGFloat thickness = [statusBar thickness]; + [image setSize:NSMakeSize(thickness, thickness)]; + if( isTemplate ) { + [image setTemplate:YES]; + } + statusItem.button.image = [image autorelease]; + statusItem.button.imagePosition = position; + }); +} + +// Add menu to system tray +void systemTraySetMenu(void* nsStatusItem, void* nsMenu) { + // Set the menu on the main thread + dispatch_async(dispatch_get_main_queue(), ^{ + NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; + NSMenu *menu = (NSMenu *)nsMenu; + statusItem.menu = menu; + }); +} + +// Destroy system tray +void systemTrayDestroy(void* nsStatusItem) { + // Remove the status item from the status bar and its associated menu + dispatch_async(dispatch_get_main_queue(), ^{ + NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; + [[NSStatusBar systemStatusBar] removeStatusItem:statusItem]; + [statusItem release]; + }); +} + +void showMenu(void* nsStatusItem) { + // Show the menu on the main thread + dispatch_async(dispatch_get_main_queue(), ^{ + NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; + // Check it's not nil + if( statusItem.menu != nil ) { + [statusItem popUpStatusItemMenu:statusItem.menu]; + } + }); +} + +void systemTrayGetBounds(void* nsStatusItem, NSRect *rect) { + NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; + NSRect buttonFrame = statusItem.button.frame; + *rect = [statusItem.button.window convertRectToScreen:buttonFrame]; +} \ No newline at end of file