Skip to content

Dismiss menu bar popover when opening Settings#481

Merged
FuJacob merged 3 commits into
FuJacob:mainfrom
jkrauska:fix/menu-bar-close-on-settings
May 31, 2026
Merged

Dismiss menu bar popover when opening Settings#481
FuJacob merged 3 commits into
FuJacob:mainfrom
jkrauska:fix/menu-bar-close-on-settings

Conversation

@jkrauska

@jkrauska jkrauska commented May 31, 2026

Copy link
Copy Markdown
Contributor

Summary

The MenuBarExtra(.window) popover stayed visible on top of the Settings window after clicking Settings from the menu bar, partially obscuring the Settings UI. SwiftUI's @Environment(\.dismiss) does not work inside MenuBarExtra(.window), so this captures the popover's host NSWindow through the same viewDidMoveToWindow hook MenuBarPresentationObserver already uses, and calls orderOut(nil) on it before invoking onOpenSettings.

The existing inline note at MenuBarView.swift:60 already acknowledged this gap — it explained that Link dismisses the popover for free but Button does not. This PR closes that gap for the Settings button.

Fixes #455

Validation

xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' \
  build -derivedDataPath build/DerivedData CODE_SIGNING_ALLOWED=NO
# ** BUILD SUCCEEDED **

xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' \
  build-for-testing -derivedDataPath build/DerivedData CODE_SIGNING_ALLOWED=NO
# ** TEST BUILD SUCCEEDED **

Tested manually: opened the menu bar dropdown, clicked Settings, and confirmed the popover dismisses cleanly before the Settings window appears — no overlap, no lingering popover. The fix is awkward to capture as a still image since the bug is "popover stays" / fix is "popover goes away"; a short screen recording is the best evidence and can be added in a follow-up comment if reviewers want it.

swiftlint lint --quiet not run locally (binary not installed on this machine); CI will gate.

Linked issues

Fixes #455

Risk / rollout notes

  • No new defaults / no migrations. Behavior change is limited to the moment the Settings button is clicked from the menu bar.
  • Other entry points to Settings are unaffected. Only the menu bar's Settings button calls the new popoverDismisser.dismiss(); the app menu / shortcut paths into the Settings window still go through SettingsCoordinator.showSettings() directly.
  • NSWindow lifetime: the dismisser holds the host window as weak, and the binder skips capture when window is nil (the teardown phase), so no stale reference can outlive the popover instance.
  • Why an NSViewRepresentable instead of \.dismiss: SwiftUI's DismissAction is documented for "current presentation" but does not bridge to the AppKit window that backs MenuBarExtra(.window). The existing MenuBarPresentationObserver already proves the viewDidMoveToWindow route is the reliable way to reach that window from SwiftUI; this PR reuses the same pattern.

Greptile Summary

This PR fixes a bug where the MenuBarExtra(.window) popover remained visible on top of the Settings window after clicking the Settings button, because SwiftUI's @Environment(\\.dismiss) does not bridge to the AppKit window backing a MenuBarExtra.

  • Introduces MenuBarPopoverDismisser, a @MainActor ObservableObject that captures the popover's host NSWindow via a weak reference using the same viewDidMoveToWindow hook already used by MenuBarPresentationObserver, and exposes a dismiss() method that calls resignKey() + orderOut(nil) + a DFS walk of NSApp.windows to unhighlight the NSStatusBarButton.
  • Introduces MenuBarPopoverDismisserBinder, an invisible NSViewRepresentable attached as a .background modifier, which forwards the real host window to the dismisser.
  • The Settings button in MenuBarView now calls popoverDismisser.dismiss() before invoking onOpenSettings(), eliminating the popover-over-Settings overlap.

Confidence Score: 5/5

Safe to merge once the local signing-identity artifact in project.pbxproj (flagged in a prior review) is reverted.

The new dismissal logic is narrowly scoped to the Settings button click path, uses weak references correctly to avoid retain cycles, and mirrors the proven pattern already in MenuBarPresentationObserver. The dismiss() → onOpenSettings() call order is synchronous and AppKit-safe. No data flow, persistence, or auth paths are touched.

Cotabby.xcodeproj/project.pbxproj — the DevelopmentTeam signing change flagged in the previous review thread still needs to be reverted before merging.

Important Files Changed

Filename Overview
Cotabby/UI/MenuBarPopoverDismisser.swift New file introducing MenuBarPopoverDismisser (captures host NSWindow via viewDidMoveToWindow) and MenuBarPopoverDismisserBinder (NSViewRepresentable bridge); logic is sound, weak references are correct, @mainactor isolation is respected because NSView methods are implicitly @mainactor in Swift 5.9+.
Cotabby/UI/MenuBarView.swift Adds @StateObject popoverDismisser, attaches the binder as a .background modifier, and updates the Settings button to call dismiss() before onOpenSettings(); change is minimal and self-contained.
Cotabby.xcodeproj/project.pbxproj Adds MenuBarPopoverDismisser.swift to the build target; also contains a local signing-identity change (DevelopmentTeam swap) already flagged in a previous review thread.

Sequence Diagram

sequenceDiagram
    participant User
    participant MenuBarView
    participant MenuBarPopoverDismisserBinder
    participant WindowBindingView
    participant MenuBarPopoverDismisser
    participant NSWindow_popover as NSWindow (popover)
    participant SettingsCoordinator

    Note over MenuBarView,WindowBindingView: On menu bar open
    MenuBarView->>MenuBarPopoverDismisserBinder: .background(binder)
    MenuBarPopoverDismisserBinder->>WindowBindingView: makeNSView / updateNSView
    WindowBindingView->>WindowBindingView: viewDidMoveToWindow()
    WindowBindingView->>MenuBarPopoverDismisser: "hostWindow = window"

    Note over User,SettingsCoordinator: User clicks Settings
    User->>MenuBarView: tap Settings button
    MenuBarView->>MenuBarPopoverDismisser: dismiss()
    MenuBarPopoverDismisser->>NSWindow_popover: resignKey()
    MenuBarPopoverDismisser->>NSWindow_popover: orderOut(nil)
    MenuBarPopoverDismisser->>MenuBarPopoverDismisser: unhighlightStatusBarButton()
    MenuBarView->>SettingsCoordinator: onOpenSettings()
    SettingsCoordinator-->>User: Settings window appears (no popover overlap)
Loading

Reviews (3): Last reviewed commit: "Unhighlight menu bar status item when di..." | Re-trigger Greptile

The MenuBarExtra(.window) popover stayed visible on top of the Settings
window when the user clicked Settings from the menu bar, obscuring part
of the Settings UI. SwiftUI's \\.dismiss action does not work for
MenuBarExtra(.window), so capture the popover's host NSWindow via an
NSViewRepresentable (the same viewDidMoveToWindow hook
MenuBarPresentationObserver uses) and orderOut it before opening the
Settings window.

Fixes FuJacob#455
Comment thread Cotabby.xcodeproj/project.pbxproj
Comment thread Cotabby/UI/MenuBarPopoverDismisser.swift
@jkrauska

Copy link
Copy Markdown
Contributor Author

jkrauska added 2 commits May 31, 2026 11:19
Xcode re-introduced its auto-signing artifact (DEVELOPMENT_TEAM in
per-config build settings, no DevelopmentTeam in TargetAttributes) the
last time the project was opened locally. Resetting the file to
upstream/main and re-running xcodegen leaves only the legitimate
MenuBarPopoverDismisser.swift rows in the diff.

Addresses Greptile P1 on FuJacob#481.
Calling orderOut on the popover window did not clear the
NSStatusBarButton's pressed-state appearance, leaving the menu bar icon
looking stuck in the "open" state after Settings appeared. The system
only clears that highlight on its own click-toggle path, so do it
explicitly: walk NSApp.windows for our NSStatusBarButton (public AppKit,
scoped to our process) and call highlight(false) once the popover has
been ordered out.

@FuJacob FuJacob left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

🚢🚢🚢🚢🚢 Thanks!

@FuJacob FuJacob merged commit f1b6319 into FuJacob:main May 31, 2026
4 checks passed
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.

[Bug] Clicking 'Settings' from menu bar should close the menu bar dropdown after opening Settings

2 participants