diff --git a/AGENTS.md b/AGENTS.md index fcc052e..fe2058e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -134,12 +134,13 @@ preferred position controls where the hidden/kept boundary lands: - Verified with `--diag-hide` across positions 100–400. Below ~200 Wi-Fi gets hidden too; above ~300 app icons (Now Playing, TextInput) leak back. - `debug.spacerPos` (Double) overrides the seed for tuning without rebuilding. -- Pressing a tray tile calls `MenuBarItemManager.press` (`kAXPressAction`, then - `kAXShowMenuAction`). Because a hidden item sits off-screen, its menu would - open off-screen; `AppDelegate` therefore calls `MenuBarHider.suspend()` - (`popoverWillShow`) to reveal items while the panel is open and `resume()` - 0.6s after `popoverDidClose`. `suspend`/`resume` do not change the user's - `enabled` preference. +- Pressing a tray tile calls `MenuBarItemManager.press`. A hidden item sits + off-screen, and its menu would open off-screen too, so press briefly + `suspend()`s the hider, waits ~0.18s for the layout to settle, performs the + accessibility action (`kAXPressAction`, then `kAXShowMenuAction`), and + `resume()`s ~1s later. The reveal is momentary, not for the whole time the + panel is open. macOS does not let us place or anchor another app's menu + anywhere else, so a momentary reveal is the only way to show it in view. - **Order matters**: `AppDelegate` must call `setupStatusItem()` *before* `MenuBarHider.shared.prepare()`. If the hider contracts the bar first, the hamburger is created into the collapsed space and lands off-screen. diff --git a/Sources/SimpleTopBar/App.swift b/Sources/SimpleTopBar/App.swift index 91d9139..4cdc96c 100644 --- a/Sources/SimpleTopBar/App.swift +++ b/Sources/SimpleTopBar/App.swift @@ -17,7 +17,7 @@ struct SimpleTopBarApp: App { /// full menu bar, a brand new item lands under the notch and is invisible; /// writing `NSStatusItem Preferred Position ` into defaults puts it in a /// visible slot, and the autosave name lets the user's own drag persist. -final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { +final class AppDelegate: NSObject, NSApplicationDelegate { private var statusItem: NSStatusItem? private var popover: NSPopover? @@ -103,7 +103,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { let popover = NSPopover() popover.behavior = .transient popover.animates = false - popover.delegate = self popover.contentViewController = NSHostingController( rootView: TopBarView( model: AppListModel.shared, @@ -114,20 +113,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { self.popover = popover } - // Reveal the hidden menu bar items while the panel is open so pressing a - // tray icon opens that app's menu at its real, on-screen position. - func popoverWillShow(_ notification: Notification) { - MenuBarHider.shared.suspend() - } - - func popoverDidClose(_ notification: Notification) { - // Small delay so a menu just opened from a tray tile is fully on screen - // before the bar contracts again. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { - MenuBarHider.shared.resume() - } - } - @objc private func togglePopover() { guard let popover, let button = statusItem?.button else { return } if popover.isShown { diff --git a/Sources/SimpleTopBar/MenuBarItems.swift b/Sources/SimpleTopBar/MenuBarItems.swift index df108c2..2112207 100644 --- a/Sources/SimpleTopBar/MenuBarItems.swift +++ b/Sources/SimpleTopBar/MenuBarItems.swift @@ -103,10 +103,30 @@ final class MenuBarItemManager: ObservableObject { } } - /// Opens the real menu bar item by pressing its accessibility element, so - /// its menu/popover appears at its true screen location. - @discardableResult - func press(_ item: MenuBarItem) -> Bool { + /// Opens the real menu bar item, so its menu/popover appears. + /// + /// A hidden item sits off-screen, and its menu would open off-screen too. + /// So briefly reveal the items, let the layout settle, press the item, then + /// hide again shortly after. The reveal is momentary rather than lasting the + /// whole time the panel is open. + func press(_ item: MenuBarItem) { + let hider = MenuBarHider.shared + let needsReveal = hider.isHiding + guard needsReveal else { + _ = performPress(item) + return + } + hider.suspend() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.18) { + _ = self.performPress(item) + // Let the menu finish appearing before the bar contracts again. + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + hider.resume() + } + } + } + + private func performPress(_ item: MenuBarItem) -> Bool { if AXUIElementPerformAction(item.element, kAXPressAction as CFString) == .success { return true }