Skip to content

Commit 81fdeee

Browse files
authored
fix(mac): restore right-click status-item menu on macOS 27 (#472)
Present the right-click menu from a global right-mouse-down monitor (hit-tested against the status-item window) via NSMenu.popUp, since macOS 27 no longer routes right-mouse events to the status-item action. Legacy action path retained for macOS <= 26, guarded by a debounce. Remove the global monitor on terminate.
1 parent f1bf7a1 commit 81fdeee

1 file changed

Lines changed: 38 additions & 3 deletions

File tree

mac/Sources/CodeBurnMenubar/CodeBurnApp.swift

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ struct CodeBurnApp: App {
3232
final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
3333
private var statusItem: NSStatusItem!
3434
private var popover: NSPopover!
35+
private var rightClickMonitor: Any?
36+
private var lastContextMenuPresentedAt: Date = .distantPast
3537
fileprivate let store = AppStore()
3638
let updateChecker = UpdateChecker()
3739
/// Held for the lifetime of the app to opt out of App Nap and Automatic Termination.
@@ -50,6 +52,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
5052
private var codexQuotaRefreshTask: Task<Bool, Never>?
5153
private var refreshLoopHeartbeatAt: Date = .distantPast
5254

55+
func applicationWillTerminate(_ notification: Notification) {
56+
if let monitor = rightClickMonitor {
57+
NSEvent.removeMonitor(monitor)
58+
rightClickMonitor = nil
59+
}
60+
}
61+
5362
func applicationWillFinishLaunching(_ notification: Notification) {
5463
// Set accessory policy before the app's focus chain forms. On macOS Tahoe
5564
// (26.x), setting it after didFinishLaunching causes ghost status items
@@ -622,8 +631,25 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
622631

623632
button.target = self
624633
button.action = #selector(handleButtonClick(_:))
634+
// Left-click drives the popover. We keep .rightMouseUp in the mask so the
635+
// legacy action path below still fires on macOS <= 26; on macOS 27 the
636+
// system consumes the right button entirely and this never fires, so the
637+
// global monitor (below) is what restores the right-click menu there.
625638
button.sendAction(on: [.leftMouseUp, .rightMouseUp])
626639

640+
// macOS 27 no longer routes any right-mouse event to the status-item
641+
// button's target/action. A global monitor still observes right-mouse-down;
642+
// we hit-test it against our own status-item window and present the menu
643+
// ourselves. Harmless and stable on 15/26 too (the debounce in
644+
// showContextMenu prevents a double-present if the legacy path also fires).
645+
rightClickMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.rightMouseDown]) { [weak self] _ in
646+
guard let self,
647+
let button = self.statusItem.button,
648+
let window = button.window,
649+
window.frame.contains(NSEvent.mouseLocation) else { return }
650+
DispatchQueue.main.async { self.showContextMenu(from: button) }
651+
}
652+
627653
// Defer the full attributed title setup to ensure initial render completes
628654
DispatchQueue.main.async { [weak self] in
629655
self?.refreshStatusButton()
@@ -783,6 +809,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
783809
guard let button = statusItem.button,
784810
let event = NSApp.currentEvent else { return }
785811

812+
// Legacy right-click path for macOS <= 26 (no-op on 27, where the action
813+
// never receives a right-mouse event — the global monitor handles it).
786814
if event.type == .rightMouseUp {
787815
showContextMenu(from: button)
788816
return
@@ -816,6 +844,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
816844
}
817845

818846
private func showContextMenu(from button: NSStatusBarButton) {
847+
// Debounce: on macOS <= 26 both the legacy action path and the global
848+
// monitor can fire for a single right-click. Present at most once per click.
849+
let now = Date()
850+
guard now.timeIntervalSince(lastContextMenuPresentedAt) > 0.3 else { return }
851+
lastContextMenuPresentedAt = now
852+
819853
let menu = NSMenu()
820854

821855
let settingsItem = NSMenuItem(title: "Settings…", action: #selector(openSettings), keyEquivalent: ",")
@@ -835,9 +869,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
835869
quitItem.target = self
836870
menu.addItem(quitItem)
837871

838-
statusItem.menu = menu
839-
button.performClick(nil)
840-
statusItem.menu = nil
872+
// Present directly. The previous `statusItem.menu = menu; button.performClick`
873+
// trick relies on the click -> action path that macOS 27 changed; popUp is
874+
// version-stable.
875+
menu.popUp(positioning: nil, at: NSPoint(x: 0, y: button.bounds.height + 4), in: button)
841876
}
842877

843878
private var settingsWindowController: NSWindowController?

0 commit comments

Comments
 (0)