From 31278269dbf5f19547dcf9a94e66e2593d40e89c Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Tue, 27 Jan 2026 07:20:38 +1100 Subject: [PATCH 1/5] fix(systray): highlight status item and ensure popup appears above other windows on macOS Fixes #4910 This commit addresses two issues with the systray-menu example on macOS: 1. System tray icon not in selected state upon clicking: - Added systemTraySetHighlight() function to control the NSStatusBarButton highlight state - Set highlight:YES before showing menus via popUpStatusItemMenu - Added onAttachedWindowShown/Hidden hooks for managing highlight with attached windows - Note: For attached windows, the highlight may briefly flash due to macOS NSStatusBarButton behavior where mouse-up clears highlight state 2. Popup doesn't display on top of other windows: - Set window level to NSPopUpMenuWindowLevel when positioning systray popup windows - Call orderFrontRegardless to ensure the window appears in front The fix adds onAttachedWindowShown() and onAttachedWindowHidden() to the systemTrayImpl interface to allow platform-specific handling when the attached window visibility changes. Co-Authored-By: Claude Opus 4.5 --- v3/pkg/application/application_server.go | 2 ++ v3/pkg/application/systemtray.go | 7 ++++ v3/pkg/application/systemtray_android.go | 8 +++++ v3/pkg/application/systemtray_darwin.go | 12 +++++++ v3/pkg/application/systemtray_darwin.h | 1 + v3/pkg/application/systemtray_darwin.m | 46 +++++++++++++++++++----- v3/pkg/application/systemtray_ios.go | 8 +++++ v3/pkg/application/systemtray_linux.go | 8 +++++ v3/pkg/application/systemtray_windows.go | 8 +++++ 9 files changed, 91 insertions(+), 9 deletions(-) diff --git a/v3/pkg/application/application_server.go b/v3/pkg/application/application_server.go index 41059242f3c..60da9e11d70 100644 --- a/v3/pkg/application/application_server.go +++ b/v3/pkg/application/application_server.go @@ -461,6 +461,8 @@ func (t *serverSystemTray) positionWindow(w Window, o int) error { return errors func (t *serverSystemTray) openMenu() {} func (t *serverSystemTray) Show() {} func (t *serverSystemTray) Hide() {} +func (t *serverSystemTray) onAttachedWindowHidden() {} +func (t *serverSystemTray) onAttachedWindowShown() {} // newWindowImpl creates a webview window implementation for server mode. func newWindowImpl(parent *WebviewWindow) *serverWebviewWindow { diff --git a/v3/pkg/application/systemtray.go b/v3/pkg/application/systemtray.go index 0735b52e277..103110d2462 100644 --- a/v3/pkg/application/systemtray.go +++ b/v3/pkg/application/systemtray.go @@ -39,6 +39,8 @@ type systemTrayImpl interface { openMenu() Show() Hide() + onAttachedWindowHidden() + onAttachedWindowShown() } type SystemTray struct { @@ -109,6 +111,8 @@ func (s *SystemTray) Run() { // Setup listener s.attachedWindow.Window.OnWindowEvent(events.Common.WindowLostFocus, func(event *WindowEvent) { s.attachedWindow.Window.Hide() + // Notify impl that attached window was hidden (e.g., to update highlight state on macOS) + s.impl.onAttachedWindowHidden() // Special handler for Windows if runtime.GOOS == "windows" { // We don't do this unless the window has already been shown @@ -322,10 +326,13 @@ func (s *SystemTray) defaultClickHandler() { if s.attachedWindow.Window.IsVisible() { s.attachedWindow.Window.Hide() + // onAttachedWindowHidden is called via WindowLostFocus event handler } else { s.attachedWindow.hasBeenShown = true _ = s.PositionWindow(s.attachedWindow.Window, s.attachedWindow.Offset) s.attachedWindow.Window.Show().Focus() + // Set highlight after window is shown (important for macOS) + s.impl.onAttachedWindowShown() } } diff --git a/v3/pkg/application/systemtray_android.go b/v3/pkg/application/systemtray_android.go index 489a5885398..748a8ee2652 100644 --- a/v3/pkg/application/systemtray_android.go +++ b/v3/pkg/application/systemtray_android.go @@ -100,3 +100,11 @@ func (s *androidSystemTray) Show() { func (s *androidSystemTray) Hide() { // Android doesn't have system tray } + +func (s *androidSystemTray) onAttachedWindowHidden() { + // Android doesn't have system tray +} + +func (s *androidSystemTray) onAttachedWindowShown() { + // Android doesn't have system tray +} diff --git a/v3/pkg/application/systemtray_darwin.go b/v3/pkg/application/systemtray_darwin.go index c88c835c07c..5ced558e465 100644 --- a/v3/pkg/application/systemtray_darwin.go +++ b/v3/pkg/application/systemtray_darwin.go @@ -72,6 +72,18 @@ func (s *macosSystemTray) openMenu() { C.showMenu(s.nsStatusItem, s.nsMenu) } +func (s *macosSystemTray) setHighlight(highlight bool) { + C.systemTraySetHighlight(s.nsStatusItem, C.bool(highlight)) +} + +func (s *macosSystemTray) onAttachedWindowHidden() { + s.setHighlight(false) +} + +func (s *macosSystemTray) onAttachedWindowShown() { + s.setHighlight(true) +} + type button int const ( diff --git a/v3/pkg/application/systemtray_darwin.h b/v3/pkg/application/systemtray_darwin.h index 29404e0959d..840cdbad901 100644 --- a/v3/pkg/application/systemtray_darwin.h +++ b/v3/pkg/application/systemtray_darwin.h @@ -17,6 +17,7 @@ NSImage* imageFromBytes(const unsigned char *bytes, int length); void systemTraySetIcon(void* nsStatusItem, void* nsImage, int position, bool isTemplate); void systemTrayDestroy(void* nsStatusItem); void showMenu(void* nsStatusItem, void *nsMenu); +void systemTraySetHighlight(void* nsStatusItem, bool highlight); void systemTrayGetBounds(void* nsStatusItem, NSRect *rect, void **screen); NSRect NSScreen_frame(void* screen); void windowSetScreen(void* window, void* screen, int yOffset); diff --git a/v3/pkg/application/systemtray_darwin.m b/v3/pkg/application/systemtray_darwin.m index a715fc125c4..77d89c43618 100644 --- a/v3/pkg/application/systemtray_darwin.m +++ b/v3/pkg/application/systemtray_darwin.m @@ -140,10 +140,32 @@ void systemTrayDestroy(void* nsStatusItem) { }); } +void systemTraySetHighlight(void* nsStatusItem, bool highlight) { + NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; + if (highlight) { + // Use dispatch_after to set highlight AFTER mouse up event clears it + // This is a known workaround for NSStatusBarButton highlight persistence + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [statusItem.button highlight:YES]; + }); + } else { + // For turning off highlight, do it immediately + if ([NSThread isMainThread]) { + [statusItem.button highlight:NO]; + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + [statusItem.button highlight:NO]; + }); + } + } +} + void showMenu(void* nsStatusItem, void *nsMenu) { // Show the menu on the main thread dispatch_async(dispatch_get_main_queue(), ^{ NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; + // Highlight the button before showing the menu + [statusItem.button highlight:YES]; [statusItem popUpStatusItemMenu:(NSMenu *)nsMenu]; // Post a mouse up event so the statusitem defocuses NSEvent *event = [NSEvent mouseEventWithType:NSEventTypeLeftMouseUp @@ -201,25 +223,25 @@ int statusBarHeight() { void systemTrayPositionWindow(void* nsStatusItem, void* nsWindow, int offset) { // Get the status item's button NSStatusBarButton *button = [(NSStatusItem*)nsStatusItem button]; - + // Get the frame in screen coordinates NSRect frame = [button.window convertRectToScreen:button.frame]; - + // Get the screen that contains the status item NSScreen *screen = [button.window screen]; if (screen == nil) { screen = [NSScreen mainScreen]; } - + // Get screen's backing scale factor (DPI) CGFloat scaleFactor = [screen backingScaleFactor]; - + // Get the window's frame NSRect windowFrame = [(NSWindow*)nsWindow frame]; - + // Calculate the horizontal position (centered under the status item) CGFloat windowX = frame.origin.x + (frame.size.width - windowFrame.size.width) / 2; - + // If the window would go off the right edge of the screen, adjust it if (windowX + windowFrame.size.width > screen.frame.origin.x + screen.frame.size.width) { windowX = screen.frame.origin.x + screen.frame.size.width - windowFrame.size.width; @@ -228,17 +250,23 @@ void systemTrayPositionWindow(void* nsStatusItem, void* nsWindow, int offset) { if (windowX < screen.frame.origin.x) { windowX = screen.frame.origin.x; } - + // Get screen metrics NSRect screenFrame = [screen frame]; NSRect visibleFrame = [screen visibleFrame]; - + // Calculate the vertical position CGFloat scaledOffset = offset * scaleFactor; CGFloat windowY = visibleFrame.origin.y + visibleFrame.size.height - windowFrame.size.height - scaledOffset; - + // Set the window's frame windowFrame.origin.x = windowX; windowFrame.origin.y = windowY; [(NSWindow*)nsWindow setFrame:windowFrame display:YES animate:NO]; + + // Set window level to popup menu level so it appears above other windows + [(NSWindow*)nsWindow setLevel:NSPopUpMenuWindowLevel]; + + // Bring window to front + [(NSWindow*)nsWindow orderFrontRegardless]; } diff --git a/v3/pkg/application/systemtray_ios.go b/v3/pkg/application/systemtray_ios.go index faa4d042753..b532e94512b 100644 --- a/v3/pkg/application/systemtray_ios.go +++ b/v3/pkg/application/systemtray_ios.go @@ -100,4 +100,12 @@ func (s *iosSystemTray) Show() { func (s *iosSystemTray) Hide() { // iOS doesn't have system tray +} + +func (s *iosSystemTray) onAttachedWindowHidden() { + // iOS doesn't have system tray +} + +func (s *iosSystemTray) onAttachedWindowShown() { + // iOS doesn't have system tray } \ No newline at end of file diff --git a/v3/pkg/application/systemtray_linux.go b/v3/pkg/application/systemtray_linux.go index df44da5860d..da335b94a95 100644 --- a/v3/pkg/application/systemtray_linux.go +++ b/v3/pkg/application/systemtray_linux.go @@ -751,6 +751,14 @@ func (s *linuxSystemTray) Hide() { // No-op } +func (s *linuxSystemTray) onAttachedWindowHidden() { + // No-op - Linux doesn't need special handling when attached window is hidden +} + +func (s *linuxSystemTray) onAttachedWindowShown() { + // No-op - Linux doesn't need special handling when attached window is shown +} + // tooltip is our data for a tooltip property. // Param names need to match the generated code... type tooltip = struct { diff --git a/v3/pkg/application/systemtray_windows.go b/v3/pkg/application/systemtray_windows.go index 55bea46e87b..7f9999bf605 100644 --- a/v3/pkg/application/systemtray_windows.go +++ b/v3/pkg/application/systemtray_windows.go @@ -588,6 +588,14 @@ func (s *windowsSystemTray) Hide() { } } +func (s *windowsSystemTray) onAttachedWindowHidden() { + // No-op - Windows doesn't need special handling when attached window is hidden +} + +func (s *windowsSystemTray) onAttachedWindowShown() { + // No-op - Windows doesn't need special handling when attached window is shown +} + func (s *windowsSystemTray) show() (w32.NOTIFYICONDATA, error) { nid := s.newNotifyIconData() nid.UFlags = w32.NIF_ICON | w32.NIF_MESSAGE From e2788e12b79ebc55dbabfbc9f4191778dfe38fb8 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Tue, 27 Jan 2026 21:44:46 +1100 Subject: [PATCH 2/5] docs: add changelog entry for systray fixes (#4910) Co-Authored-By: Claude Opus 4.5 --- v3/UNRELEASED_CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index 8e46480384f..e9af4543219 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -23,6 +23,8 @@ After processing, the content will be moved to the main changelog and this file ## Fixed +- Fix system tray menu highlight state on macOS - icon now shows selected state when menu is open (#4910) +- Fix system tray attached window appearing behind other windows on macOS - now uses proper popup window level (#4910) ## Deprecated From 34819563364046858c1116f98282f3be006fc438 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Wed, 28 Jan 2026 06:21:37 +1100 Subject: [PATCH 3/5] chore: trigger fresh CI run Co-Authored-By: Claude Opus 4.5 From 0d8d462e5d2f7a061ba98fbf3c67049a006bc35f Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Wed, 28 Jan 2026 06:35:53 +1100 Subject: [PATCH 4/5] Simplify systray fix - remove unused window highlight code Remove onAttachedWindowShown/Hidden interface methods and systemTraySetHighlight function since they only worked for menus, not attached windows. The menu highlight (set before popUpStatusItemMenu) and window level fixes remain. Co-Authored-By: Claude Opus 4.5 --- v3/pkg/application/application_server.go | 2 -- v3/pkg/application/systemtray.go | 7 ------- v3/pkg/application/systemtray_android.go | 8 -------- v3/pkg/application/systemtray_darwin.go | 12 ------------ v3/pkg/application/systemtray_darwin.h | 1 - v3/pkg/application/systemtray_darwin.m | 20 -------------------- v3/pkg/application/systemtray_ios.go | 8 -------- v3/pkg/application/systemtray_linux.go | 8 -------- v3/pkg/application/systemtray_windows.go | 8 -------- 9 files changed, 74 deletions(-) diff --git a/v3/pkg/application/application_server.go b/v3/pkg/application/application_server.go index 60da9e11d70..41059242f3c 100644 --- a/v3/pkg/application/application_server.go +++ b/v3/pkg/application/application_server.go @@ -461,8 +461,6 @@ func (t *serverSystemTray) positionWindow(w Window, o int) error { return errors func (t *serverSystemTray) openMenu() {} func (t *serverSystemTray) Show() {} func (t *serverSystemTray) Hide() {} -func (t *serverSystemTray) onAttachedWindowHidden() {} -func (t *serverSystemTray) onAttachedWindowShown() {} // newWindowImpl creates a webview window implementation for server mode. func newWindowImpl(parent *WebviewWindow) *serverWebviewWindow { diff --git a/v3/pkg/application/systemtray.go b/v3/pkg/application/systemtray.go index 103110d2462..0735b52e277 100644 --- a/v3/pkg/application/systemtray.go +++ b/v3/pkg/application/systemtray.go @@ -39,8 +39,6 @@ type systemTrayImpl interface { openMenu() Show() Hide() - onAttachedWindowHidden() - onAttachedWindowShown() } type SystemTray struct { @@ -111,8 +109,6 @@ func (s *SystemTray) Run() { // Setup listener s.attachedWindow.Window.OnWindowEvent(events.Common.WindowLostFocus, func(event *WindowEvent) { s.attachedWindow.Window.Hide() - // Notify impl that attached window was hidden (e.g., to update highlight state on macOS) - s.impl.onAttachedWindowHidden() // Special handler for Windows if runtime.GOOS == "windows" { // We don't do this unless the window has already been shown @@ -326,13 +322,10 @@ func (s *SystemTray) defaultClickHandler() { if s.attachedWindow.Window.IsVisible() { s.attachedWindow.Window.Hide() - // onAttachedWindowHidden is called via WindowLostFocus event handler } else { s.attachedWindow.hasBeenShown = true _ = s.PositionWindow(s.attachedWindow.Window, s.attachedWindow.Offset) s.attachedWindow.Window.Show().Focus() - // Set highlight after window is shown (important for macOS) - s.impl.onAttachedWindowShown() } } diff --git a/v3/pkg/application/systemtray_android.go b/v3/pkg/application/systemtray_android.go index 748a8ee2652..489a5885398 100644 --- a/v3/pkg/application/systemtray_android.go +++ b/v3/pkg/application/systemtray_android.go @@ -100,11 +100,3 @@ func (s *androidSystemTray) Show() { func (s *androidSystemTray) Hide() { // Android doesn't have system tray } - -func (s *androidSystemTray) onAttachedWindowHidden() { - // Android doesn't have system tray -} - -func (s *androidSystemTray) onAttachedWindowShown() { - // Android doesn't have system tray -} diff --git a/v3/pkg/application/systemtray_darwin.go b/v3/pkg/application/systemtray_darwin.go index 5ced558e465..c88c835c07c 100644 --- a/v3/pkg/application/systemtray_darwin.go +++ b/v3/pkg/application/systemtray_darwin.go @@ -72,18 +72,6 @@ func (s *macosSystemTray) openMenu() { C.showMenu(s.nsStatusItem, s.nsMenu) } -func (s *macosSystemTray) setHighlight(highlight bool) { - C.systemTraySetHighlight(s.nsStatusItem, C.bool(highlight)) -} - -func (s *macosSystemTray) onAttachedWindowHidden() { - s.setHighlight(false) -} - -func (s *macosSystemTray) onAttachedWindowShown() { - s.setHighlight(true) -} - type button int const ( diff --git a/v3/pkg/application/systemtray_darwin.h b/v3/pkg/application/systemtray_darwin.h index 840cdbad901..29404e0959d 100644 --- a/v3/pkg/application/systemtray_darwin.h +++ b/v3/pkg/application/systemtray_darwin.h @@ -17,7 +17,6 @@ NSImage* imageFromBytes(const unsigned char *bytes, int length); void systemTraySetIcon(void* nsStatusItem, void* nsImage, int position, bool isTemplate); void systemTrayDestroy(void* nsStatusItem); void showMenu(void* nsStatusItem, void *nsMenu); -void systemTraySetHighlight(void* nsStatusItem, bool highlight); void systemTrayGetBounds(void* nsStatusItem, NSRect *rect, void **screen); NSRect NSScreen_frame(void* screen); void windowSetScreen(void* window, void* screen, int yOffset); diff --git a/v3/pkg/application/systemtray_darwin.m b/v3/pkg/application/systemtray_darwin.m index 77d89c43618..afb0bb1f00f 100644 --- a/v3/pkg/application/systemtray_darwin.m +++ b/v3/pkg/application/systemtray_darwin.m @@ -140,26 +140,6 @@ void systemTrayDestroy(void* nsStatusItem) { }); } -void systemTraySetHighlight(void* nsStatusItem, bool highlight) { - NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; - if (highlight) { - // Use dispatch_after to set highlight AFTER mouse up event clears it - // This is a known workaround for NSStatusBarButton highlight persistence - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [statusItem.button highlight:YES]; - }); - } else { - // For turning off highlight, do it immediately - if ([NSThread isMainThread]) { - [statusItem.button highlight:NO]; - } else { - dispatch_async(dispatch_get_main_queue(), ^{ - [statusItem.button highlight:NO]; - }); - } - } -} - void showMenu(void* nsStatusItem, void *nsMenu) { // Show the menu on the main thread dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/v3/pkg/application/systemtray_ios.go b/v3/pkg/application/systemtray_ios.go index b532e94512b..faa4d042753 100644 --- a/v3/pkg/application/systemtray_ios.go +++ b/v3/pkg/application/systemtray_ios.go @@ -100,12 +100,4 @@ func (s *iosSystemTray) Show() { func (s *iosSystemTray) Hide() { // iOS doesn't have system tray -} - -func (s *iosSystemTray) onAttachedWindowHidden() { - // iOS doesn't have system tray -} - -func (s *iosSystemTray) onAttachedWindowShown() { - // iOS doesn't have system tray } \ No newline at end of file diff --git a/v3/pkg/application/systemtray_linux.go b/v3/pkg/application/systemtray_linux.go index da335b94a95..df44da5860d 100644 --- a/v3/pkg/application/systemtray_linux.go +++ b/v3/pkg/application/systemtray_linux.go @@ -751,14 +751,6 @@ func (s *linuxSystemTray) Hide() { // No-op } -func (s *linuxSystemTray) onAttachedWindowHidden() { - // No-op - Linux doesn't need special handling when attached window is hidden -} - -func (s *linuxSystemTray) onAttachedWindowShown() { - // No-op - Linux doesn't need special handling when attached window is shown -} - // tooltip is our data for a tooltip property. // Param names need to match the generated code... type tooltip = struct { diff --git a/v3/pkg/application/systemtray_windows.go b/v3/pkg/application/systemtray_windows.go index 7f9999bf605..55bea46e87b 100644 --- a/v3/pkg/application/systemtray_windows.go +++ b/v3/pkg/application/systemtray_windows.go @@ -588,14 +588,6 @@ func (s *windowsSystemTray) Hide() { } } -func (s *windowsSystemTray) onAttachedWindowHidden() { - // No-op - Windows doesn't need special handling when attached window is hidden -} - -func (s *windowsSystemTray) onAttachedWindowShown() { - // No-op - Windows doesn't need special handling when attached window is shown -} - func (s *windowsSystemTray) show() (w32.NOTIFYICONDATA, error) { nid := s.newNotifyIconData() nid.UFlags = w32.NIF_ICON | w32.NIF_MESSAGE From cb38f0706d61c3e4368ddcf6d9ce6d47c66a0806 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sat, 7 Feb 2026 12:43:06 +1100 Subject: [PATCH 5/5] fix(v3/macos): use native menu tracking for systray highlight and focus (#4910) Replace popUpStatusItemMenu: (which never provided native highlighting) with a local NSEvent monitor that temporarily sets statusItem.menu before the button processes mouse-down events. This lets macOS handle menu tracking natively, which: - Keeps the status bar button highlighted while the menu is open - Does not activate the app or steal focus from other windows Co-Authored-By: Claude Opus 4.6 --- v3/pkg/application/systemtray_darwin.go | 38 ++++++++++++ v3/pkg/application/systemtray_darwin.h | 6 +- v3/pkg/application/systemtray_darwin.m | 82 ++++++++++++++++++++----- 3 files changed, 109 insertions(+), 17 deletions(-) diff --git a/v3/pkg/application/systemtray_darwin.go b/v3/pkg/application/systemtray_darwin.go index c88c835c07c..51804b86d3e 100644 --- a/v3/pkg/application/systemtray_darwin.go +++ b/v3/pkg/application/systemtray_darwin.go @@ -93,12 +93,48 @@ func systrayClickCallback(id C.long, buttonID C.int) { systemTray.processClick(button(buttonID)) } +// systrayPreClickCallback is called from the NSEvent local monitor BEFORE the +// button processes the mouse-down. It returns 1 when the framework should +// show the menu via native tracking (proper highlight, no app activation), +// or 0 to let the action handler fire for custom click/window behaviour. +// +//export systrayPreClickCallback +func systrayPreClickCallback(id C.long, buttonID C.int) C.int { + systemTray := systemTrayMap[uint(id)] + if systemTray == nil || systemTray.nsMenu == nil { + return 0 + } + b := button(buttonID) + switch b { + case leftButtonDown: + if systemTray.parent.clickHandler == nil && + systemTray.parent.attachedWindow.Window == nil { + return 1 + } + case rightButtonDown: + if systemTray.parent.rightClickHandler == nil { + // Hide the attached window before the menu appears. + if systemTray.parent.attachedWindow.Window != nil && + systemTray.parent.attachedWindow.Window.IsVisible() { + systemTray.parent.attachedWindow.Window.Hide() + } + return 1 + } + } + return 0 +} + func (s *macosSystemTray) setIconPosition(position IconPosition) { s.iconPosition = position } func (s *macosSystemTray) setMenu(menu *Menu) { s.menu = menu + if s.nsStatusItem != nil && menu != nil { + menu.Update() + s.nsMenu = (menu.impl).(*macosMenu).nsMenu + C.systemTraySetCachedMenu(s.nsStatusItem, s.nsMenu) + } } func (s *macosSystemTray) positionWindow(window Window, offset int) error { @@ -167,6 +203,8 @@ func (s *macosSystemTray) run() { s.menu.Update() // Convert impl to macosMenu object s.nsMenu = (s.menu.impl).(*macosMenu).nsMenu + // Cache on the ObjC controller for the event monitor. + C.systemTraySetCachedMenu(s.nsStatusItem, s.nsMenu) } }) } diff --git a/v3/pkg/application/systemtray_darwin.h b/v3/pkg/application/systemtray_darwin.h index 29404e0959d..e1f55d817d1 100644 --- a/v3/pkg/application/systemtray_darwin.h +++ b/v3/pkg/application/systemtray_darwin.h @@ -2,8 +2,11 @@ #include -@interface StatusItemController : NSObject +@interface StatusItemController : NSObject @property long id; +@property (assign) NSStatusItem *statusItem; +@property (assign) NSMenu *cachedMenu; +@property (strong) id eventMonitor; - (void)statusItemClicked:(id)sender; @end @@ -17,6 +20,7 @@ NSImage* imageFromBytes(const unsigned char *bytes, int length); void systemTraySetIcon(void* nsStatusItem, void* nsImage, int position, bool isTemplate); void systemTrayDestroy(void* nsStatusItem); void showMenu(void* nsStatusItem, void *nsMenu); +void systemTraySetCachedMenu(void* nsStatusItem, void *nsMenu); void systemTrayGetBounds(void* nsStatusItem, NSRect *rect, void **screen); NSRect NSScreen_frame(void* screen); void windowSetScreen(void* window, void* screen, int yOffset); diff --git a/v3/pkg/application/systemtray_darwin.m b/v3/pkg/application/systemtray_darwin.m index afb0bb1f00f..31b7ac62756 100644 --- a/v3/pkg/application/systemtray_darwin.m +++ b/v3/pkg/application/systemtray_darwin.m @@ -5,6 +5,7 @@ #include "systemtray_darwin.h" extern void systrayClickCallback(long, int); +extern int systrayPreClickCallback(long, int); // StatusItemController.m @implementation StatusItemController @@ -14,6 +15,13 @@ - (void)statusItemClicked:(id)sender { systrayClickCallback(self.id, event.type); } +- (void)menuDidClose:(NSMenu *)menu { + // Remove the menu from the status item so future clicks invoke the + // action handler instead of re-showing the menu. + self.statusItem.menu = nil; + menu.delegate = nil; +} + @end // Create a new system tray @@ -21,10 +29,29 @@ - (void)statusItemClicked:(id)sender { StatusItemController *controller = [[StatusItemController alloc] init]; controller.id = id; NSStatusItem *statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength] retain]; + controller.statusItem = statusItem; [statusItem setTarget:controller]; [statusItem setAction:@selector(statusItemClicked:)]; NSButton *button = statusItem.button; [button sendActionOn:(NSEventMaskLeftMouseDown|NSEventMaskRightMouseDown)]; + + // Install a local event monitor that fires BEFORE the button processes + // the mouse-down. When the pre-click callback says "show menu", we + // temporarily set statusItem.menu so the button enters native menu + // tracking — this gives proper highlight and does not activate the app. + controller.eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask: + (NSEventMaskLeftMouseDown|NSEventMaskRightMouseDown) + handler:^NSEvent *(NSEvent *event) { + if (event.window != button.window) return event; + + int action = systrayPreClickCallback((long)controller.id, (int)event.type); + if (action == 1 && controller.cachedMenu != nil) { + controller.cachedMenu.delegate = controller; + statusItem.menu = controller.cachedMenu; + } + return event; + }]; + return (void*)statusItem; } @@ -135,33 +162,56 @@ 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; + StatusItemController *controller = (StatusItemController *)[statusItem target]; + if (controller.eventMonitor) { + [NSEvent removeMonitor:controller.eventMonitor]; + controller.eventMonitor = nil; + } [[NSStatusBar systemStatusBar] removeStatusItem:statusItem]; + [controller release]; [statusItem release]; }); } +// showMenu is used for programmatic OpenMenu() calls. Click-triggered +// menus are handled by the event monitor installed in systemTrayNew. void showMenu(void* nsStatusItem, void *nsMenu) { - // Show the menu on the main thread dispatch_async(dispatch_get_main_queue(), ^{ NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; - // Highlight the button before showing the menu - [statusItem.button highlight:YES]; - [statusItem popUpStatusItemMenu:(NSMenu *)nsMenu]; - // Post a mouse up event so the statusitem defocuses - NSEvent *event = [NSEvent mouseEventWithType:NSEventTypeLeftMouseUp - location:[NSEvent mouseLocation] - modifierFlags:0 - timestamp:[[NSProcessInfo processInfo] systemUptime] - windowNumber:0 - context:nil - eventNumber:0 - clickCount:1 - pressure:1]; - [NSApp postEvent:event atStart:NO]; - [statusItem.button highlight:NO]; + NSMenu *menu = (NSMenu *)nsMenu; + StatusItemController *controller = (StatusItemController *)[statusItem target]; + + // Temporarily assign the menu for native tracking. + menu.delegate = controller; + statusItem.menu = menu; + + // Synthesize a mouse-down at the button centre to trigger native + // menu tracking (highlights the button, blocks until dismissed). + NSRect frame = [statusItem.button convertRect:statusItem.button.bounds toView:nil]; + NSPoint loc = NSMakePoint(NSMidX(frame), NSMidY(frame)); + NSEvent *event = [NSEvent mouseEventWithType:NSEventTypeLeftMouseDown + location:loc + modifierFlags:0 + timestamp:[[NSProcessInfo processInfo] systemUptime] + windowNumber:statusItem.button.window.windowNumber + context:nil + eventNumber:0 + clickCount:1 + pressure:1.0]; + [statusItem.button mouseDown:event]; + + // Menu dismissed — restore custom click handling. + statusItem.menu = nil; + menu.delegate = nil; }); } +void systemTraySetCachedMenu(void* nsStatusItem, void *nsMenu) { + NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; + StatusItemController *controller = (StatusItemController *)[statusItem target]; + controller.cachedMenu = (NSMenu *)nsMenu; +} + void systemTrayGetBounds(void* nsStatusItem, NSRect *rect, void **outScreen) { NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; NSStatusBarButton *button = statusItem.button;