Skip to content

Conversation

@leaanthony
Copy link
Member

@leaanthony leaanthony commented Jan 27, 2026

Summary

Fixes #4910

This PR addresses two issues with the systray-menu example on macOS:

  • System tray icon not in selected state upon clicking: The status item button now properly highlights when clicked and menu is shown
  • Popup doesn't display on top of other windows: The attached window now appears above other applications

Changes

Highlight state fix

  • 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

Window level fix

  • Modified systemTrayPositionWindow() to set NSPopUpMenuWindowLevel on the window
  • Added orderFrontRegardless to ensure the window appears in front

Test plan

  • Run v3/examples/systray-menu on macOS
  • Click the W icon in systray - verify icon is highlighted while menu is visible
  • Verify popup window appears above other applications
  • Right-click to show menu - verify highlight behavior

Notes

  • For attached windows (not menus), the highlight may briefly flash due to macOS NSStatusBarButton behavior where mouse-up clears highlight state. This is a known limitation.
  • Branch renamed from bugfix/* to v3-bugfix/* to avoid triggering v2 CI workflow

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • System tray menu highlight state on macOS now correctly shows the icon as selected when the menu is open.
    • System tray attached windows on macOS no longer appear behind other windows; they now surface reliably.

leaanthony and others added 2 commits January 27, 2026 21:37
…her 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>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 27, 2026

Walkthrough

Adds a pre-click hook and menu caching to the macOS system tray; uses native menu tracking to ensure the status item shows selected state and sets the attached popup window to NSPopUpMenuWindowLevel so it appears above other windows.

Changes

Cohort / File(s) Summary
Changelog
v3/UNRELEASED_CHANGELOG.md
Added two Fixed entries: macOS system tray menu highlight state and attached window layering (#4910).
macOS ObjC implementation
v3/pkg/application/systemtray_darwin.m, v3/pkg/application/systemtray_darwin.h
Added StatusItemController NSMenuDelegate integration, menuDidClose: handler, cachedMenu and eventMonitor properties, systemTraySetCachedMenu API, native menu tracking flow, and set popup window level to NSPopUpMenuWindowLevel with orderFrontRegardless to ensure topmost display.
macOS Go bindings / pre-click logic
v3/pkg/application/systemtray_darwin.go
Introduced systrayPreClickCallback to decide native vs. custom handling pre-click, cache nsMenu for event monitor, and adjust setMenu/run to populate cached menu for native tracking.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant macOS as System (NSStatusItem)
  participant ObjC as StatusItemController
  participant Go as Go runtime (systray)
  participant Menu as NSMenu/AttachedWindow

  User->>macOS: click (left/right)
  macOS->>ObjC: pre-click event
  ObjC->>Go: systrayPreClickCallback(id, button)
  alt native tracking allowed (returns 1)
    Go-->>ObjC: 1 (allow native)
    ObjC->>macOS: assign cachedMenu to statusItem.menu
    ObjC->>macOS: simulate click to trigger native menu tracking
    macOS->>Menu: show native menu (menu highlights status item)
  else custom handling (returns 0)
    Go-->>ObjC: 0 (custom)
    ObjC->>Go: invoke custom click handlers / show attached window
    Go->>Menu: show attached window (set level NSPopUpMenuWindowLevel)
  end
  Menu->>ObjC: menuDidClose
  ObjC->>macOS: restore previous menu state / clear delegate
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

MacOS, Bug, v3-alpha, size:S

Poem

🐰 I hopped to the tray with a curious cheer,
Clicked once and the icon showed bright and clear,
When windows would hide, I nudged them to top,
Now menus and popups behave — hip-hop!
A tiny rabbit dance for fixes near.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete. It lacks the required checklist items and test configuration details, with only some checklist items marked. Complete all checklist items and provide the output of 'wails doctor' test configuration as required by the repository template.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main changes: fixing status item highlight and popup window level on macOS systray.
Linked Issues check ✅ Passed The PR addresses the two main objectives from issue #4910: system tray icon highlighting and popup window appearing above other windows on macOS.
Out of Scope Changes check ✅ Passed All code changes in the PR are directly related to fixing the two issues in #4910 regarding systray highlighting and window level behavior on macOS.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch v3-bugfix/4910-systray-highlight-and-popup-level

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Jan 27, 2026

Deploying wails with  Cloudflare Pages  Cloudflare Pages

Latest commit: cb38f07
Status: ✅  Deploy successful!
Preview URL: https://eff97e0a.wails.pages.dev
Branch Preview URL: https://v3-bugfix-4910-systray-highl.wails.pages.dev

View logs

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>
@sonarqubecloud
Copy link

@sjayach
Copy link

sjayach commented Jan 27, 2026

@leaanthony it's still not highlighting(not in selected state). but other app icon is highlighting (zoom app , etc.,)
image

One more issue.
Once system tray opened then existing previous window like chrome or other app lose focus (maximize, minimize and close buttons are greyed out ) but it's not happening for other native system tray apps like wifi, battery or zoom app etc.,

…us (#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>
Copilot AI review requested due to automatic review settings February 7, 2026 01:43
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes two macOS issues in the v3 system tray implementation: ensuring the status item shows a proper highlighted/selected state while menus are open, and making attached “popup” windows appear above other applications.

Changes:

  • Add a pre-click callback and an NSEvent local monitor to enable native menu tracking for better status item highlight behavior.
  • Rework showMenu() to trigger native menu tracking via a synthesized mouse-down and clean up menu assignment on close.
  • Raise attached windows to NSPopUpMenuWindowLevel (and attempt to bring them forward) when positioned from the system tray.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
v3/pkg/application/systemtray_darwin.m Adds event monitor + native menu tracking changes; adjusts window positioning level/ordering.
v3/pkg/application/systemtray_darwin.h Extends StatusItemController to support NSMenuDelegate and stores status item/menu/monitor references.
v3/pkg/application/systemtray_darwin.go Adds exported pre-click callback and caches NSMenu pointer on the ObjC controller.
v3/UNRELEASED_CHANGELOG.md Adds changelog entries for the two macOS fixes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +42 to +51
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;
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The local NSEvent monitor only checks event.window != button.window to decide whether to handle the click. All status bar items share the same status bar window, so this monitor can run for clicks on other menu bar items and may leave statusItem.menu set without ever opening/closing the menu (breaking subsequent click behavior). Add a stricter hit-test (e.g., ensure the event location is within button.frame / hitTest: equals the button) before calling systrayPreClickCallback / setting statusItem.menu.

Copilot uses AI. Check for mistakes.
Comment on lines +133 to +136
if s.nsStatusItem != nil && menu != nil {
menu.Update()
s.nsMenu = (menu.impl).(*macosMenu).nsMenu
C.systemTraySetCachedMenu(s.nsStatusItem, s.nsMenu)
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

systemTraySetCachedMenu is only called when menu != nil, so calling SetMenu(nil) leaves s.nsMenu and the ObjC cachedMenu pointing at the previous menu. Because systrayPreClickCallback gates on systemTray.nsMenu != nil, right-click can still show the stale menu after it has been cleared. When menu == nil, explicitly set s.nsMenu = nil and clear the cached menu on the ObjC controller as well.

Suggested change
if s.nsStatusItem != nil && menu != nil {
menu.Update()
s.nsMenu = (menu.impl).(*macosMenu).nsMenu
C.systemTraySetCachedMenu(s.nsStatusItem, s.nsMenu)
if s.nsStatusItem == nil {
return
}
if menu != nil {
menu.Update()
s.nsMenu = (menu.impl).(*macosMenu).nsMenu
C.systemTraySetCachedMenu(s.nsStatusItem, s.nsMenu)
} else {
s.nsMenu = nil
C.systemTraySetCachedMenu(s.nsStatusItem, nil)

Copilot uses AI. Check for mistakes.
Comment on lines +300 to +301
// Bring window to front
[(NSWindow*)nsWindow orderFrontRegardless];
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

systemTrayPositionWindow() now calls orderFrontRegardless, which has visible side effects (it can show/raise the window) even when the caller only intends to reposition it. Since SystemTray.PositionWindow() is a public API, this changes behavior beyond the attached-window use case. Consider removing orderFrontRegardless from positioning, or at least gating it on [(NSWindow*)nsWindow isVisible] / moving the bring-to-front behavior to the code path that actually shows the attached window.

Suggested change
// Bring window to front
[(NSWindow*)nsWindow orderFrontRegardless];
// Bring window to front only if it is already visible, to avoid changing
// behavior of callers that only intend to reposition the window.
if ([(NSWindow*)nsWindow isVisible]) {
[(NSWindow*)nsWindow orderFrontRegardless];
}

Copilot uses AI. Check for mistakes.
Comment on lines +176 to +177
// showMenu is used for programmatic OpenMenu() calls. Click-triggered
// menus are handled by the event monitor installed in systemTrayNew.
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment above showMenu() says click-triggered menus are handled by the event monitor, but the Go click path can still call showMenu() (e.g., via defaultClickHandler -> OpenMenu()), and left-click currently won’t take the event-monitor branch because clickHandler is non-nil by default. Update the comment to reflect the actual call paths so future changes don’t rely on incorrect assumptions.

Suggested change
// showMenu is used for programmatic OpenMenu() calls. Click-triggered
// menus are handled by the event monitor installed in systemTrayNew.
// showMenu is used for programmatic OpenMenu() calls and for menus opened
// from Go click handlers (e.g., defaultClickHandler -> OpenMenu()). Some
// click-triggered menus may also be initiated by the event monitor installed
// in systemTrayNew, but they still ultimately flow through this function.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@v3/pkg/application/systemtray_darwin.go`:
- Around line 131-138: The call to C.systemTraySetCachedMenu and the assignment
of s.nsMenu must happen on the main thread to avoid races with the event
monitor; modify macosSystemTray.setMenu so that after computing ns :=
(menu.impl).(*macosMenu).nsMenu you dispatch a block to the main queue (e.g. via
a C helper that uses dispatch_async(dispatch_get_main_queue(), ...)) which sets
s.nsMenu = ns and calls C.systemTraySetCachedMenu(s.nsStatusItem, ns), leaving
menu.Update() on the caller goroutine; ensure you reference setMenu,
s.nsStatusItem, s.nsMenu, macosMenu.nsMenu and C.systemTraySetCachedMenu when
making the change.

In `@v3/pkg/application/systemtray_darwin.m`:
- Around line 296-301: The code in systemTrayPositionWindow sets the window
level to NSPopUpMenuWindowLevel and calls orderFrontRegardless, which can leave
other apps inactive and produce greyed-out controls; change this to use
NSStatusWindowLevel or convert the window to a non-activating panel (NSPanel
with NSWindowStyleMaskNonactivatingPanel) so the popup overlays without
deactivating the app, and replace orderFrontRegardless with a fronting call
appropriate for non-activating panels; also ensure the level change is applied
only when showing the tray popup (and restore or avoid persisting
NSPopUpMenuWindowLevel) so the window does not remain at popup level after
repositioning.
🧹 Nitpick comments (1)
v3/pkg/application/systemtray_darwin.go (1)

293-324: processClick menu paths are now unreachable for click-triggered events.

With the event monitor in place, whenever systrayPreClickCallback returns 1, native menu tracking handles the menu and the button action (systrayClickCallbackprocessClick) never fires. The C.showMenu(…) calls on lines 306 and 318 are now only reachable via the programmatic openMenu() path.

Consider adding a brief comment noting this, so future maintainers understand these branches serve only the programmatic OpenMenu() codepath.

Comment on lines 131 to 138
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)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

systemTraySetCachedMenu is called outside the main thread.

systemTraySetCachedMenu in the .m file accesses ObjC objects directly without dispatch_async(dispatch_get_main_queue(), …). If setMenu is ever called from a non-main goroutine while the event monitor is reading cachedMenu, this is a data race.

The same concern applies to the existing s.nsMenu assignment on line 135, so this is a pre-existing pattern — but the new cachedMenu is actively read by the event monitor on the main thread, increasing the risk.

Suggested fix: dispatch the cache update to the main thread

In systemtray_darwin.m:

 void systemTraySetCachedMenu(void* nsStatusItem, void *nsMenu) {
-	NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem;
-	StatusItemController *controller = (StatusItemController *)[statusItem target];
-	controller.cachedMenu = (NSMenu *)nsMenu;
+	dispatch_async(dispatch_get_main_queue(), ^{
+		NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem;
+		StatusItemController *controller = (StatusItemController *)[statusItem target];
+		controller.cachedMenu = (NSMenu *)nsMenu;
+	});
 }
🤖 Prompt for AI Agents
In `@v3/pkg/application/systemtray_darwin.go` around lines 131 - 138, The call to
C.systemTraySetCachedMenu and the assignment of s.nsMenu must happen on the main
thread to avoid races with the event monitor; modify macosSystemTray.setMenu so
that after computing ns := (menu.impl).(*macosMenu).nsMenu you dispatch a block
to the main queue (e.g. via a C helper that uses
dispatch_async(dispatch_get_main_queue(), ...)) which sets s.nsMenu = ns and
calls C.systemTraySetCachedMenu(s.nsStatusItem, ns), leaving menu.Update() on
the caller goroutine; ensure you reference setMenu, s.nsStatusItem, s.nsMenu,
macosMenu.nsMenu and C.systemTraySetCachedMenu when making the change.

Comment on lines +296 to +301

// Set window level to popup menu level so it appears above other windows
[(NSWindow*)nsWindow setLevel:NSPopUpMenuWindowLevel];

// Bring window to front
[(NSWindow*)nsWindow orderFrontRegardless];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

NSPopUpMenuWindowLevel likely causes the greyed-out controls reported by the reviewer.

NSPopUpMenuWindowLevel (101) combined with orderFrontRegardless (no app activation) puts the window above everything but leaves the previously focused app in an "inactive" state — hence greyed-out close/minimize/maximize buttons in apps like Chrome.

Native macOS status-item popups (Wi-Fi, Bluetooth, etc.) typically use an NSPanel with NSWindowStyleMaskNonactivatingPanel so they can overlay other windows without deactivating them. Consider:

  1. Using NSStatusWindowLevel (25) instead — still above normal windows but less aggressive.
  2. Or configuring the attached window as a non-activating panel, which is the standard macOS pattern for status-item popups.

Additionally, this level is set every time systemTrayPositionWindow is called but never reset, so the window permanently stays at popup-menu level even if it's later repositioned or used differently.

🤖 Prompt for AI Agents
In `@v3/pkg/application/systemtray_darwin.m` around lines 296 - 301, The code in
systemTrayPositionWindow sets the window level to NSPopUpMenuWindowLevel and
calls orderFrontRegardless, which can leave other apps inactive and produce
greyed-out controls; change this to use NSStatusWindowLevel or convert the
window to a non-activating panel (NSPanel with
NSWindowStyleMaskNonactivatingPanel) so the popup overlays without deactivating
the app, and replace orderFrontRegardless with a fronting call appropriate for
non-activating panels; also ensure the level change is applied only when showing
the tray popup (and restore or avoid persisting NSPopUpMenuWindowLevel) so the
window does not remain at popup level after repositioning.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants