mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
fix(systray): highlight status item and ensure popup appears above other windows on macOS (#4913)
* 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 <noreply@anthropic.com> * docs: add changelog entry for systray fixes (#4910) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: trigger fresh CI run Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b4b0b90fff
commit
5d6e9872d4
4 changed files with 126 additions and 24 deletions
|
|
@ -24,6 +24,8 @@ After processing, the content will be moved to the main changelog and this file
|
|||
|
||||
## Fixed
|
||||
<!-- Bug fixes -->
|
||||
- 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)
|
||||
- Fix incorrect `@wailsio/runtime` import examples across documentation (#4989)
|
||||
|
||||
## Deprecated
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@
|
|||
|
||||
#include <Cocoa/Cocoa.h>
|
||||
|
||||
@interface StatusItemController : NSObject
|
||||
@interface StatusItemController : NSObject <NSMenuDelegate>
|
||||
@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);
|
||||
|
|
|
|||
|
|
@ -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 @@ extern void systrayClickCallback(long, int);
|
|||
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* systemTrayNew(long id) {
|
|||
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,31 +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;
|
||||
[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;
|
||||
|
|
@ -201,25 +253,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 +280,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];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue