Skip to content

Conversation

@leaanthony
Copy link
Member

@leaanthony leaanthony commented Dec 22, 2025

Summary

Implements NSMenuDelegate to enable real-time menu updates while the system tray menu is open on macOS. Previously, menu.Update() calls would not refresh the displayed menu until it was closed and reopened.

Also implements onMenuOpen and onMenuClose callbacks for macOS, bringing it to parity with Windows and Linux.

Changes

  • Added MenuDelegate class implementing NSMenuDelegate protocol
  • Implemented menuNeedsUpdate:, menuWillOpen:, and menuDidClose: delegate methods
  • Added nsMenuDelegate field to macosSystemTray struct
  • Updated setMenu() to create and attach delegate to NSMenu
  • Updated run() to set up delegate during initialization
  • Added Go callback exports systrayMenuOpenCallback and systrayMenuCloseCallback

How it works

  1. MenuDelegate is attached to the NSMenu when menu is set
  2. When menu.Update() is called, NSMenu is rebuilt in-place
  3. macOS calls menuNeedsUpdate: before display and periodically
  4. menuWillOpen: triggers onMenuOpen callback
  5. menuDidClose: triggers onMenuClose callback

Platform parity

Feature macOS Linux Windows
Real-time menu updates ✅ Now supported ✅ DBus signals ✅ Win32 APIs
onMenuOpen callback ✅ Now supported
onMenuClose callback ✅ Now supported

Fixes #4719

🤖 Generated with Claude Code

Implements NSMenuDelegate to enable real-time menu updates while the
system tray menu is open on macOS. Previously, menu.Update() calls
would not refresh the displayed menu until it was closed and reopened.

Changes:
- Added MenuDelegate class implementing NSMenuDelegate protocol
- Implemented menuNeedsUpdate: and menuWillOpen: delegate methods
- Added nsMenuDelegate field to macosSystemTray struct
- Updated setMenu() to create and attach delegate to NSMenu
- Updated run() to set up delegate during initialization

Fixes #4630

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 22, 2025

Walkthrough

This change implements a macOS system tray menu delegate pattern to enable real-time menu updates. A new MenuDelegate Objective-C class is introduced to conform to NSMenuDelegate, with delegate methods that respond to menu lifecycle events. The Go code wires this delegate to the NSMenu during both initial setup and menu updates, replacing manual update points with automatic delegate-based callbacks.

Changes

Cohort / File(s) Change Summary
Changelog Documentation
v3/UNRELEASED_CHANGELOG.md
Added fixed entry documenting the macOS system tray menu real-time update fix.
Go Integration Layer
v3/pkg/application/systemtray_darwin.go
Added nsMenuDelegate field to macosSystemTray struct. Modified setMenu() to initialize and assign menu delegate on main thread. Updated run() to initialize delegate after NSStatusItem creation, enabling delegate-based real-time updates.
Objective-C Header
v3/pkg/application/systemtray_darwin.h
Introduced new MenuDelegate interface conforming to NSMenuDelegate with menuPtr and trayID properties. Added function declarations: createMenuDelegate(), setMenuDelegate(), and retained systemTrayPositionWindow() declaration.
Objective-C Implementation
v3/pkg/application/systemtray_darwin.m
Implemented MenuDelegate class with menuNeedsUpdate: and menuWillOpen: lifecycle methods. Added createMenuDelegate() factory function and setMenuDelegate() attachment function. Note: duplicate function definitions appear at end of file.

Sequence Diagram

sequenceDiagram
    participant Go as Go Code
    participant Tray as macOS SystemTray
    participant Delegate as MenuDelegate
    participant NSMenu as NSMenu (native)
    
    Note over Go,NSMenu: Initial Setup (run)
    Go->>Tray: run() initializes NSStatusItem
    Tray->>Delegate: createMenuDelegate(menuPtr, trayID)
    Delegate-->>Tray: return delegate instance
    Tray->>NSMenu: setMenuDelegate(nsMenu, delegate)
    NSMenu->>Delegate: assign as delegate
    
    Note over Go,NSMenu: Real-time Update Flow (user opens menu)
    NSMenu->>Delegate: menuNeedsUpdate:(menu)
    Delegate-->>Go: triggers on-demand callback
    Go->>NSMenu: menu reflects live changes
    
    Note over Go,NSMenu: Menu Update (programmatic)
    Go->>Tray: setMenu(newMenu) on main thread
    Tray->>Delegate: initialize/assign delegate if needed
    Tray->>NSMenu: setMenuDelegate(nsMenu, delegate)
    NSMenu->>Delegate: delegate now active
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

  • Objective-C pattern adoption: Review delegate lifecycle hooks (menuNeedsUpdate:, menuWillOpen:) and verify they integrate correctly with Go side menu rebuilding
  • Go/Objective-C boundary: Verify unsafe pointer handling in nsMenuDelegate field and delegate initialization/assignment logic
  • Duplicate code: The note that "duplicated set of these two functions appears at the end of the file" suggests potential cleanup or copy-paste error that should be verified
  • Thread safety: Confirm main thread dispatch in setMenu() is correctly applied to all delegate operations

Suggested labels

MacOS, v3-alpha, Enhancement

Poem

A rabbit hops through macOS lanes,
where menus now obey delegate chains,
Real-time updates dance and gleam,
when NSMenuDelegate fulfills the dream. 🐰✨

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: fixing macOS system tray menu real-time updates using NSMenuDelegate.
Linked Issues check ✅ Passed The code changes successfully implement NSMenuDelegate to enable real-time menu updates on macOS when menu.Update() is called, directly addressing the requirement in issue #4630.
Out of Scope Changes check ✅ Passed All changes are focused on implementing menu delegation for macOS system tray real-time updates; no unrelated modifications are present.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The PR description provides a comprehensive summary of changes, includes the fixed issue reference (#4719), and follows the general structure of the template.
✨ 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 fix/systemtray-menu-realtime-update

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
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: 0

🧹 Nitpick comments (2)
v3/pkg/application/systemtray_darwin.go (1)

114-118: Delegate's menuPtr may become stale on subsequent menu changes.

The delegate is created once (when nsMenuDelegate == nil) with the current s.nsMenu pointer. If setMenu is called again with a different menu, the delegate retains the old menuPtr reference. While the current delegate implementation doesn't use menuPtr, this could cause issues if future changes rely on it.

Consider updating menuPtr on every call or documenting this limitation:

🔎 Suggested fix
 		// Set up the delegate if not already done
 		if s.nsMenuDelegate == nil {
 			s.nsMenuDelegate = C.createMenuDelegate(s.nsMenu, C.long(s.id))
+		} else {
+			// Update the delegate's menuPtr if menu changed
+			C.updateMenuDelegatePtr(s.nsMenuDelegate, s.nsMenu)
 		}
 		C.setMenuDelegate(s.nsMenu, s.nsMenuDelegate)

This would require adding an updateMenuDelegatePtr function in the Objective-C layer.

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

262-267: Consider delegate lifecycle management.

The MenuDelegate is allocated with [[MenuDelegate alloc] init] but there's no corresponding release when the system tray is destroyed. While the delegate likely lives for the application lifetime, if system trays can be dynamically created/destroyed, this could leak memory.

If needed, store the delegate reference and release it in systemTrayDestroy:

// In systemTrayDestroy, if delegate is tracked:
// [delegate release];
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0271c9e and 0742b75.

📒 Files selected for processing (4)
  • v3/UNRELEASED_CHANGELOG.md
  • v3/pkg/application/systemtray_darwin.go
  • v3/pkg/application/systemtray_darwin.h
  • v3/pkg/application/systemtray_darwin.m
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-01-24T22:41:18.566Z
Learnt from: leaanthony
Repo: wailsapp/wails PR: 4031
File: v3/pkg/application/menu.go:199-202
Timestamp: 2025-01-24T22:41:18.566Z
Learning: In the Wails menu system (v3/pkg/application/menu.go), shared state between menus is intentionally designed and desirable. Methods like `Append()` and `Prepend()` should maintain shared references to menu items rather than creating deep copies.

Applied to files:

  • v3/pkg/application/systemtray_darwin.go
🪛 LanguageTool
v3/UNRELEASED_CHANGELOG.md

[style] ~26-~26: This phrase is redundant (‘OS’ stands for ‘operating system’). Use simply “macOS”.
Context: ... --> ## Fixed - Fix macOS system tray menu real-time updates using NSMen...

(ACRONYM_TAUTOLOGY)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Run Go Tests v3 (ubuntu-latest, 1.24)
  • GitHub Check: Run Go Tests v3 (windows-latest, 1.24)
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: Analyze (go)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (7)
v3/UNRELEASED_CHANGELOG.md (1)

26-26: LGTM!

The changelog entry clearly describes the fix and references the correct issue number. Note: The static analysis warning about "macOS" is a false positive—"macOS" is Apple's official branding and is correct here.

v3/pkg/application/systemtray_darwin.go (2)

48-48: LGTM!

The new nsMenuDelegate field appropriately holds the Objective-C delegate reference using unsafe.Pointer for CGo interop.


188-190: LGTM!

The delegate setup during initial run() is correctly placed after menu.Update() ensures the nsMenu pointer is valid.

v3/pkg/application/systemtray_darwin.h (2)

10-13: LGTM!

The MenuDelegate interface declaration correctly adopts NSMenuDelegate protocol with appropriate properties for CGo interop.


30-31: LGTM!

Function declarations are well-defined and match the implementation signatures.

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

19-32: Clarify the real-time update mechanism.

Both delegate methods are effectively no-ops. The comment says "The Go side will handle the actual menu rebuilding through menu.Update()" but there's no callback to Go from these methods.

Could you clarify: does simply having a delegate attached cause macOS to refresh the menu display when the underlying NSMenu items change? If the intent is for menuNeedsUpdate: to trigger Go-side updates, a callback would be needed.


270-274: LGTM!

The setMenuDelegate function correctly casts and assigns the delegate to the NSMenu.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Dec 22, 2025

Deploying wails with  Cloudflare Pages  Cloudflare Pages

Latest commit: 86f96d5
Status: ✅  Deploy successful!
Preview URL: https://96019536.wails.pages.dev
Branch Preview URL: https://fix-systemtray-menu-realtime.wails.pages.dev

View logs

Adds NSMenuDelegate methods menuWillOpen: and menuDidClose: to call the
existing onMenuOpen and onMenuClose callbacks on SystemTray, bringing
macOS to parity with Windows and Linux implementations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@sonarqubecloud
Copy link

@johnmaguire
Copy link

In #4719, I reported two behaviors:

  1. When updating the menu, clicking a menu item from the prior render does not work. This behavior persists when using this branch:
Scheduling update in 5 seconds
Update scheduled
5:06PM INF Binding call complete: id=Mu9FLGqqYsU4r1kNshHHI method=main.UpdateService.Update args=[5] result=""
Updating menu...
5:06PM WRN MenuItem #1 not found
5:06PM INF Menu 1 clicked!
  1. When using SetMenu to replace the menu entirely previously I observed that nothing happened. This behavior persists.

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.

3 participants