Skip to content

Commit 3127826

Browse files
leaanthonyclaude
andcommitted
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>
1 parent f91b8cf commit 3127826

9 files changed

Lines changed: 91 additions & 9 deletions

v3/pkg/application/application_server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,8 @@ func (t *serverSystemTray) positionWindow(w Window, o int) error { return errors
461461
func (t *serverSystemTray) openMenu() {}
462462
func (t *serverSystemTray) Show() {}
463463
func (t *serverSystemTray) Hide() {}
464+
func (t *serverSystemTray) onAttachedWindowHidden() {}
465+
func (t *serverSystemTray) onAttachedWindowShown() {}
464466

465467
// newWindowImpl creates a webview window implementation for server mode.
466468
func newWindowImpl(parent *WebviewWindow) *serverWebviewWindow {

v3/pkg/application/systemtray.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ type systemTrayImpl interface {
3939
openMenu()
4040
Show()
4141
Hide()
42+
onAttachedWindowHidden()
43+
onAttachedWindowShown()
4244
}
4345

4446
type SystemTray struct {
@@ -109,6 +111,8 @@ func (s *SystemTray) Run() {
109111
// Setup listener
110112
s.attachedWindow.Window.OnWindowEvent(events.Common.WindowLostFocus, func(event *WindowEvent) {
111113
s.attachedWindow.Window.Hide()
114+
// Notify impl that attached window was hidden (e.g., to update highlight state on macOS)
115+
s.impl.onAttachedWindowHidden()
112116
// Special handler for Windows
113117
if runtime.GOOS == "windows" {
114118
// We don't do this unless the window has already been shown
@@ -322,10 +326,13 @@ func (s *SystemTray) defaultClickHandler() {
322326

323327
if s.attachedWindow.Window.IsVisible() {
324328
s.attachedWindow.Window.Hide()
329+
// onAttachedWindowHidden is called via WindowLostFocus event handler
325330
} else {
326331
s.attachedWindow.hasBeenShown = true
327332
_ = s.PositionWindow(s.attachedWindow.Window, s.attachedWindow.Offset)
328333
s.attachedWindow.Window.Show().Focus()
334+
// Set highlight after window is shown (important for macOS)
335+
s.impl.onAttachedWindowShown()
329336
}
330337
}
331338

v3/pkg/application/systemtray_android.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,11 @@ func (s *androidSystemTray) Show() {
100100
func (s *androidSystemTray) Hide() {
101101
// Android doesn't have system tray
102102
}
103+
104+
func (s *androidSystemTray) onAttachedWindowHidden() {
105+
// Android doesn't have system tray
106+
}
107+
108+
func (s *androidSystemTray) onAttachedWindowShown() {
109+
// Android doesn't have system tray
110+
}

v3/pkg/application/systemtray_darwin.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,18 @@ func (s *macosSystemTray) openMenu() {
7272
C.showMenu(s.nsStatusItem, s.nsMenu)
7373
}
7474

75+
func (s *macosSystemTray) setHighlight(highlight bool) {
76+
C.systemTraySetHighlight(s.nsStatusItem, C.bool(highlight))
77+
}
78+
79+
func (s *macosSystemTray) onAttachedWindowHidden() {
80+
s.setHighlight(false)
81+
}
82+
83+
func (s *macosSystemTray) onAttachedWindowShown() {
84+
s.setHighlight(true)
85+
}
86+
7587
type button int
7688

7789
const (

v3/pkg/application/systemtray_darwin.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ NSImage* imageFromBytes(const unsigned char *bytes, int length);
1717
void systemTraySetIcon(void* nsStatusItem, void* nsImage, int position, bool isTemplate);
1818
void systemTrayDestroy(void* nsStatusItem);
1919
void showMenu(void* nsStatusItem, void *nsMenu);
20+
void systemTraySetHighlight(void* nsStatusItem, bool highlight);
2021
void systemTrayGetBounds(void* nsStatusItem, NSRect *rect, void **screen);
2122
NSRect NSScreen_frame(void* screen);
2223
void windowSetScreen(void* window, void* screen, int yOffset);

v3/pkg/application/systemtray_darwin.m

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,32 @@ void systemTrayDestroy(void* nsStatusItem) {
140140
});
141141
}
142142

143+
void systemTraySetHighlight(void* nsStatusItem, bool highlight) {
144+
NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem;
145+
if (highlight) {
146+
// Use dispatch_after to set highlight AFTER mouse up event clears it
147+
// This is a known workaround for NSStatusBarButton highlight persistence
148+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
149+
[statusItem.button highlight:YES];
150+
});
151+
} else {
152+
// For turning off highlight, do it immediately
153+
if ([NSThread isMainThread]) {
154+
[statusItem.button highlight:NO];
155+
} else {
156+
dispatch_async(dispatch_get_main_queue(), ^{
157+
[statusItem.button highlight:NO];
158+
});
159+
}
160+
}
161+
}
162+
143163
void showMenu(void* nsStatusItem, void *nsMenu) {
144164
// Show the menu on the main thread
145165
dispatch_async(dispatch_get_main_queue(), ^{
146166
NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem;
167+
// Highlight the button before showing the menu
168+
[statusItem.button highlight:YES];
147169
[statusItem popUpStatusItemMenu:(NSMenu *)nsMenu];
148170
// Post a mouse up event so the statusitem defocuses
149171
NSEvent *event = [NSEvent mouseEventWithType:NSEventTypeLeftMouseUp
@@ -201,25 +223,25 @@ int statusBarHeight() {
201223
void systemTrayPositionWindow(void* nsStatusItem, void* nsWindow, int offset) {
202224
// Get the status item's button
203225
NSStatusBarButton *button = [(NSStatusItem*)nsStatusItem button];
204-
226+
205227
// Get the frame in screen coordinates
206228
NSRect frame = [button.window convertRectToScreen:button.frame];
207-
229+
208230
// Get the screen that contains the status item
209231
NSScreen *screen = [button.window screen];
210232
if (screen == nil) {
211233
screen = [NSScreen mainScreen];
212234
}
213-
235+
214236
// Get screen's backing scale factor (DPI)
215237
CGFloat scaleFactor = [screen backingScaleFactor];
216-
238+
217239
// Get the window's frame
218240
NSRect windowFrame = [(NSWindow*)nsWindow frame];
219-
241+
220242
// Calculate the horizontal position (centered under the status item)
221243
CGFloat windowX = frame.origin.x + (frame.size.width - windowFrame.size.width) / 2;
222-
244+
223245
// If the window would go off the right edge of the screen, adjust it
224246
if (windowX + windowFrame.size.width > screen.frame.origin.x + screen.frame.size.width) {
225247
windowX = screen.frame.origin.x + screen.frame.size.width - windowFrame.size.width;
@@ -228,17 +250,23 @@ void systemTrayPositionWindow(void* nsStatusItem, void* nsWindow, int offset) {
228250
if (windowX < screen.frame.origin.x) {
229251
windowX = screen.frame.origin.x;
230252
}
231-
253+
232254
// Get screen metrics
233255
NSRect screenFrame = [screen frame];
234256
NSRect visibleFrame = [screen visibleFrame];
235-
257+
236258
// Calculate the vertical position
237259
CGFloat scaledOffset = offset * scaleFactor;
238260
CGFloat windowY = visibleFrame.origin.y + visibleFrame.size.height - windowFrame.size.height - scaledOffset;
239-
261+
240262
// Set the window's frame
241263
windowFrame.origin.x = windowX;
242264
windowFrame.origin.y = windowY;
243265
[(NSWindow*)nsWindow setFrame:windowFrame display:YES animate:NO];
266+
267+
// Set window level to popup menu level so it appears above other windows
268+
[(NSWindow*)nsWindow setLevel:NSPopUpMenuWindowLevel];
269+
270+
// Bring window to front
271+
[(NSWindow*)nsWindow orderFrontRegardless];
244272
}

v3/pkg/application/systemtray_ios.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,12 @@ func (s *iosSystemTray) Show() {
100100

101101
func (s *iosSystemTray) Hide() {
102102
// iOS doesn't have system tray
103+
}
104+
105+
func (s *iosSystemTray) onAttachedWindowHidden() {
106+
// iOS doesn't have system tray
107+
}
108+
109+
func (s *iosSystemTray) onAttachedWindowShown() {
110+
// iOS doesn't have system tray
103111
}

v3/pkg/application/systemtray_linux.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,14 @@ func (s *linuxSystemTray) Hide() {
751751
// No-op
752752
}
753753

754+
func (s *linuxSystemTray) onAttachedWindowHidden() {
755+
// No-op - Linux doesn't need special handling when attached window is hidden
756+
}
757+
758+
func (s *linuxSystemTray) onAttachedWindowShown() {
759+
// No-op - Linux doesn't need special handling when attached window is shown
760+
}
761+
754762
// tooltip is our data for a tooltip property.
755763
// Param names need to match the generated code...
756764
type tooltip = struct {

v3/pkg/application/systemtray_windows.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,14 @@ func (s *windowsSystemTray) Hide() {
588588
}
589589
}
590590

591+
func (s *windowsSystemTray) onAttachedWindowHidden() {
592+
// No-op - Windows doesn't need special handling when attached window is hidden
593+
}
594+
595+
func (s *windowsSystemTray) onAttachedWindowShown() {
596+
// No-op - Windows doesn't need special handling when attached window is shown
597+
}
598+
591599
func (s *windowsSystemTray) show() (w32.NOTIFYICONDATA, error) {
592600
nid := s.newNotifyIconData()
593601
nid.UFlags = w32.NIF_ICON | w32.NIF_MESSAGE

0 commit comments

Comments
 (0)