Skip to content

fix(v3/darwin): systray highlight and attached window focus stealing#4963

Open
leaanthony wants to merge 2 commits intov3-alphafrom
v3-bugfix/systray-highlight-focus
Open

fix(v3/darwin): systray highlight and attached window focus stealing#4963
leaanthony wants to merge 2 commits intov3-alphafrom
v3-bugfix/systray-highlight-focus

Conversation

@leaanthony
Copy link
Member

@leaanthony leaanthony commented Feb 6, 2026

Summary

Fixes #4910

  • Systray icon highlight: Set [statusItem.button highlight:YES] before popUpStatusItemMenu: so the icon visually responds to clicks, matching native macOS behavior
  • Focus stealing: Show attached windows using orderFrontRegardless instead of makeKeyAndOrderFront + activateIgnoringOtherApps:YES, preventing the app from stealing focus from other applications when the systray icon is clicked
  • On macOS, applySmartDefaults no longer sets the default click handler — processClick handles attached window toggling directly via a new toggleAttachedWindow() method

Test plan

  • Click systray icon with a menu attached — icon should highlight while menu is open
  • Click systray icon with an attached window — window should appear without stealing focus from the currently focused app
  • Click systray icon again — attached window should hide
  • Right-click systray icon with both menu and attached window — window hides, menu shows with highlight
  • Verify Windows/Linux systray behavior is unchanged (no code path changes for those platforms)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • System tray window on macOS now toggles without stealing focus
    • Added visual highlighting when the system tray menu is displayed
    • Improved window positioning and display behavior on macOS systems
    • Refined cross-platform click handling for consistent system tray behavior

…4910)

Fix two macOS systray issues:
- Set button highlight before showing popup menu so the icon visually
  responds to clicks
- Show attached windows with orderFrontRegardless instead of
  activateIgnoringOtherApps to prevent stealing focus from other apps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 6, 2026 11:12
@github-actions
Copy link
Contributor

github-actions bot commented Feb 6, 2026

⚠️ Missing Changelog Update

Hi @leaanthony, please update v3/UNRELEASED_CHANGELOG.md with a description of your changes.

This helps us keep track of changes for the next release.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 6, 2026

Walkthrough

Changes implement platform-specific system tray click handling and window management for macOS. A new focus-respecting window toggle method replaces default click behavior on macOS, while cross-platform fallback logic is added for other operating systems. Native helpers expose highlight state control and focus-less window display.

Changes

Cohort / File(s) Summary
Platform-Conditional Click Handling
v3/pkg/application/systemtray.go
Modified applySmartDefaults to conditionally assign clickHandler based on OS. Non-macOS systems default to ToggleWindow if no handler is set; macOS leaves it nil for custom handling in processClick.
macOS Window Toggle Logic
v3/pkg/application/systemtray_darwin.go
Added toggleAttachedWindow() method that shows/hides attached window without stealing focus. Updated processClick for both left and right button clicks to use this method instead of default handling.
macOS Native Declarations
v3/pkg/application/systemtray_darwin.h
Declared two new public C functions: systemTraySetHighlight() for highlight state control and systemTrayShowWindowWithoutActivation() for focus-less window display.
macOS Native Implementation
v3/pkg/application/systemtray_darwin.m
Implemented highlight enforcement when displaying system tray menu and added native helper functions to set button highlight state and show windows without activating them.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

Bug, MacOS, v3-alpha, size:M

Poem

🐰 A hop through the tray so fine,
Clicks now highlight with perfect design,
Windows toggle, no focus they steal,
macOS dreams with native appeal!
Focus respect makes the magic real!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: fixing systray highlight and preventing focus stealing on macOS, which is the primary objective of this PR.
Description check ✅ Passed The PR description includes issue reference (#4910), clear summary of changes, specific implementation details, and a comprehensive test plan, meeting the template requirements.
Linked Issues check ✅ Passed The code changes directly address both requirements in issue #4910: systray icon highlighting is now set before menu display, and focus stealing is prevented by using orderFrontRegardless instead of makeKeyAndOrderFront.
Out of Scope Changes check ✅ Passed All changes are within scope—modifications to systemtray.go, systemtray_darwin.go, systemtray_darwin.h, and systemtray_darwin.m are specifically targeted at fixing the reported macOS systray issues without unrelated alterations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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/systray-highlight-focus

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.

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

Fixes macOS system tray UX issues by improving status item highlight behavior and preventing attached windows from stealing focus.

Changes:

  • Highlight the NSStatusItem button before showing the status item menu on macOS
  • Add native helpers for setting highlight state and showing a window without activating the app
  • Move macOS attached-window toggle behavior into processClick (and avoid setting default click handler for darwin)

Reviewed changes

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

File Description
v3/pkg/application/systemtray_darwin.m Highlights status item before menu popup; adds new native helpers for highlight and non-activating window show
v3/pkg/application/systemtray_darwin.h Exposes new native helper function declarations to cgo
v3/pkg/application/systemtray_darwin.go Adds macOS-specific attached window toggling that avoids app activation
v3/pkg/application/systemtray.go Skips setting default click handler on macOS so darwin processClick can handle window toggling

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

Comment on lines 146 to 149
NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem;
[statusItem.button highlight:YES];
[statusItem popUpStatusItemMenu:(NSMenu *)nsMenu];
// Post a mouse up event so the statusitem defocuses
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

highlight:YES is set before popUpStatusItemMenu: but never explicitly cleared. Since popUpStatusItemMenu: returns after menu tracking finishes, consider calling [statusItem.button highlight:NO]; immediately after it returns (or in a finally-style pattern) to avoid the icon remaining highlighted in cases where the synthetic mouse-up doesn’t clear the highlight state.

Copilot uses AI. Check for mistakes.
int statusBarHeight();
void systemTrayPositionWindow(void* nsStatusItem, void* nsWindow, int offset); No newline at end of file
void systemTrayPositionWindow(void* nsStatusItem, void* nsWindow, int offset);
void systemTraySetHighlight(void* nsStatusItem, bool highlighted);
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

bool requires <stdbool.h> in C/Objective-C headers (otherwise it may not compile under cgo/clang). Add #include <stdbool.h> to this header (or change the parameter type to _Bool / BOOL consistently with the rest of the native API).

Copilot uses AI. Check for mistakes.
Comment on lines +170 to +171
NSWindow *window = (NSWindow *)nsWindow;
[window orderFrontRegardless];
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

AppKit APIs must be called on the main thread. showMenu already dispatches to the main queue, but systemTrayShowWindowWithoutActivation doesn’t. To avoid thread-safety issues/crashes, wrap the orderFrontRegardless call in dispatch_async(dispatch_get_main_queue(), ^{ ... }); (or otherwise guarantee main-thread execution at this boundary).

Suggested change
NSWindow *window = (NSWindow *)nsWindow;
[window orderFrontRegardless];
dispatch_async(dispatch_get_main_queue(), ^{
NSWindow *window = (NSWindow *)nsWindow;
[window orderFrontRegardless];
});

Copilot uses AI. Check for mistakes.
_ = s.parent.PositionWindow(w.Window, w.Offset)
nativeWindow := w.Window.NativeWindow()
if nativeWindow != nil {
C.systemTrayShowWindowWithoutActivation(nativeWindow)
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

If NativeWindow() returns nil, the window never gets shown in the else branch. Consider adding a fallback path (e.g., call the cross-platform Show()/equivalent) so clicking the tray icon still makes the attached window visible even when NativeWindow() is temporarily unavailable.

Suggested change
C.systemTrayShowWindowWithoutActivation(nativeWindow)
C.systemTrayShowWindowWithoutActivation(nativeWindow)
} else {
// Fallback: ensure the window is still shown if the native window is unavailable.
w.Window.Show()

Copilot uses AI. Check for mistakes.
Comment on lines +309 to 312
// Handle attached window toggle without stealing focus
if s.parent.attachedWindow.Window != nil {
s.parent.defaultClickHandler()
s.toggleAttachedWindow()
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

This attached-window toggle block appears redundant/unreachable in the leftButtonDown flow because earlier in the same case you already check attachedWindow.Window != nil, call toggleAttachedWindow(), and return. Consider removing this duplicate block or restructuring so there’s a single, clearly reachable toggle path.

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 258-278: The function macosSystemTray.toggleAttachedWindow
currently copies s.parent.attachedWindow into a local struct (w), so mutations
to w.initialClick and w.hasBeenShown are lost; change the function to operate on
a pointer to the parent's WindowAttachConfig (e.g. wp :=
&s.parent.attachedWindow) and use wp throughout: check wp.Window for nil, call
wp.initialClick.Do(...), set wp.hasBeenShown, pass wp.Window and wp.Offset into
s.parent.PositionWindow, and use wp.Window.NativeWindow() before calling
C.systemTrayShowWindowWithoutActivation so all state changes affect the original
struct rather than a temporary copy.

In `@v3/pkg/application/systemtray_darwin.m`:
- Around line 164-167: The function systemTraySetHighlight is unused and should
be removed or documented: either delete its implementation here and remove its
declaration from the header (and any exported symbol registrations) to avoid
dead code, or keep it but add a clear TODO comment above systemTraySetHighlight
explaining its intended future use, why it remains declared in the header, and
mark it static/internal if it should not be exported; locate the implementation
referencing NSStatusItem and statusItem.button highlight and the corresponding
declaration in the header to apply the change.
🧹 Nitpick comments (3)
v3/pkg/application/systemtray.go (2)

109-115: Consider a build-tag-based approach instead of runtime.GOOS.

This file is shared across all platforms (no build tags). The project convention (per other platform-specific files) is to use compile-time build constraints rather than runtime OS checks. A cleaner alternative would be to extract the darwin-specific default into a platform-specific file (e.g., a systraySmartDefaults_darwin.go / systraySmartDefaults_other.go pair or a small hook function), keeping applySmartDefaults free of runtime.GOOS checks.

Note that line 131 (ToggleWindow) and line 155 (defaultClickHandler) also have runtime.GOOS == "windows" checks—those would benefit from the same treatment in a follow-up.

Based on learnings: "Reviewers should ensure there are no runtime OS checks in system.go and that platform-specific behavior is controlled via build tags. If runtime switches exist, remove them in favor of compile-time platform constraints to reduce overhead and improve correctness."


144-166: Remove the unused defaultClickHandler method.

The function is unreachable dead code. On darwin, processClick routes attached window toggle directly to toggleAttachedWindow(). On non-darwin platforms, applySmartDefaults sets clickHandler = s.ToggleWindow for attached windows, and Windows provides a fallback empty callback. defaultClickHandler is never called in production code—only in test benchmarks.

v3/pkg/application/systemtray_darwin.m (1)

169-172: Ensure this is only called from the main thread.

orderFrontRegardless is an AppKit call that must execute on the main thread. Currently it's called from toggleAttachedWindowprocessClicksystrayClickCallback, which originates from a UI event on the main thread, so this should be safe. However, unlike showMenu (which wraps in dispatch_async), this function has no main-thread guard—if it's ever called from a background context in the future, it would silently break.

Consider wrapping with dispatch_async(dispatch_get_main_queue(), ...) for defensive safety, or at minimum add a comment noting the main-thread requirement.

Comment on lines +258 to +278
func (s *macosSystemTray) toggleAttachedWindow() {
w := s.parent.attachedWindow
if w.Window == nil {
return
}

w.initialClick.Do(func() {
w.hasBeenShown = w.Window.IsVisible()
})

if w.Window.IsVisible() {
w.Window.Hide()
} else {
w.hasBeenShown = true
_ = s.parent.PositionWindow(w.Window, w.Offset)
nativeWindow := w.Window.NativeWindow()
if nativeWindow != nil {
C.systemTrayShowWindowWithoutActivation(nativeWindow)
}
}
}
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 | 🔴 Critical

Bug: w is a value copy of attachedWindow—mutations and sync.Once.Do are lost.

WindowAttachConfig is a struct (not a pointer), so w := s.parent.attachedWindow creates a shallow copy. This means:

  1. sync.Once copy: w.initialClick.Do(...) fires on the copy; the original's initialClick remains in the "not done" state, so it will fire again if anything calls ToggleWindow or defaultClickHandler on the original.
  2. hasBeenShown mutations lost: Lines 265 and 271 set hasBeenShown on the copy, which is discarded at the end of the function. The original struct retains its old value.

Use a pointer to the parent's struct instead:

🐛 Fix: use a pointer instead of a copy
 func (s *macosSystemTray) toggleAttachedWindow() {
-	w := s.parent.attachedWindow
+	w := &s.parent.attachedWindow
 	if w.Window == nil {
 		return
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (s *macosSystemTray) toggleAttachedWindow() {
w := s.parent.attachedWindow
if w.Window == nil {
return
}
w.initialClick.Do(func() {
w.hasBeenShown = w.Window.IsVisible()
})
if w.Window.IsVisible() {
w.Window.Hide()
} else {
w.hasBeenShown = true
_ = s.parent.PositionWindow(w.Window, w.Offset)
nativeWindow := w.Window.NativeWindow()
if nativeWindow != nil {
C.systemTrayShowWindowWithoutActivation(nativeWindow)
}
}
}
func (s *macosSystemTray) toggleAttachedWindow() {
w := &s.parent.attachedWindow
if w.Window == nil {
return
}
w.initialClick.Do(func() {
w.hasBeenShown = w.Window.IsVisible()
})
if w.Window.IsVisible() {
w.Window.Hide()
} else {
w.hasBeenShown = true
_ = s.parent.PositionWindow(w.Window, w.Offset)
nativeWindow := w.Window.NativeWindow()
if nativeWindow != nil {
C.systemTrayShowWindowWithoutActivation(nativeWindow)
}
}
}
🤖 Prompt for AI Agents
In `@v3/pkg/application/systemtray_darwin.go` around lines 258 - 278, The function
macosSystemTray.toggleAttachedWindow currently copies s.parent.attachedWindow
into a local struct (w), so mutations to w.initialClick and w.hasBeenShown are
lost; change the function to operate on a pointer to the parent's
WindowAttachConfig (e.g. wp := &s.parent.attachedWindow) and use wp throughout:
check wp.Window for nil, call wp.initialClick.Do(...), set wp.hasBeenShown, pass
wp.Window and wp.Offset into s.parent.PositionWindow, and use
wp.Window.NativeWindow() before calling C.systemTrayShowWindowWithoutActivation
so all state changes affect the original struct rather than a temporary copy.

Comment on lines +164 to +167
void systemTraySetHighlight(void* nsStatusItem, bool highlighted) {
NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem;
[statusItem.button highlight:highlighted];
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if systemTraySetHighlight is called from Go code
rg -nP 'systemTraySetHighlight' --type=go

Repository: wailsapp/wails

Length of output: 40


🏁 Script executed:

# Also check the header file to understand the declaration
fd -e h | xargs rg -l 'systemTraySetHighlight'

Repository: wailsapp/wails

Length of output: 96


🏁 Script executed:

# Check all occurrences across the entire codebase
rg -n 'systemTraySetHighlight'

Repository: wailsapp/wails

Length of output: 277


🏁 Script executed:

# Search in Objective-C/C files for calls to systemTraySetHighlight
rg -n 'systemTraySetHighlight' --type-list | grep -E '\.(m|c)$'
rg -n 'systemTraySetHighlight' v3/pkg/application/

Repository: wailsapp/wails

Length of output: 684


Remove unused function systemTraySetHighlight.

This function is declared in the header and implemented here, but has no callers in the codebase. Either remove it or add a comment explaining its intended future use.

🤖 Prompt for AI Agents
In `@v3/pkg/application/systemtray_darwin.m` around lines 164 - 167, The function
systemTraySetHighlight is unused and should be removed or documented: either
delete its implementation here and remove its declaration from the header (and
any exported symbol registrations) to avoid dead code, or keep it but add a
clear TODO comment above systemTraySetHighlight explaining its intended future
use, why it remains declared in the header, and mark it static/internal if it
should not be exported; locate the implementation referencing NSStatusItem and
statusItem.button highlight and the corresponding declaration in the header to
apply the change.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 6, 2026

Deploying wails with  Cloudflare Pages  Cloudflare Pages

Latest commit: dadc2bd
Status: ✅  Deploy successful!
Preview URL: https://69e22458.wails.pages.dev
Branch Preview URL: https://v3-bugfix-systray-highlight.wails.pages.dev

View logs

@github-actions
Copy link
Contributor

github-actions bot commented Feb 8, 2026

⚠️ Missing Changelog Update

Hi @leaanthony, please update v3/UNRELEASED_CHANGELOG.md with a description of your changes.

This helps us keep track of changes for the next release.

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