Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2312e09105 | ||
|
|
9c5416d27e |
@@ -134,12 +134,17 @@ preferred position controls where the hidden/kept boundary lands:
|
|||||||
- Verified with `--diag-hide` across positions 100–400. Below ~200 Wi-Fi gets
|
- Verified with `--diag-hide` across positions 100–400. Below ~200 Wi-Fi gets
|
||||||
hidden too; above ~300 app icons (Now Playing, TextInput) leak back.
|
hidden too; above ~300 app icons (Now Playing, TextInput) leak back.
|
||||||
- `debug.spacerPos` (Double) overrides the seed for tuning without rebuilding.
|
- `debug.spacerPos` (Double) overrides the seed for tuning without rebuilding.
|
||||||
- Pressing a tray tile calls `MenuBarItemManager.press` (`kAXPressAction`, then
|
- Pressing a tray tile calls `MenuBarItemManager.press`. A hidden item sits
|
||||||
`kAXShowMenuAction`). Because a hidden item sits off-screen, its menu would
|
off-screen, and its menu opens off-screen too (verified with `--diag-press`:
|
||||||
open off-screen; `AppDelegate` therefore calls `MenuBarHider.suspend()`
|
the menu window lands at negative x). So press briefly `suspend()`s the hider,
|
||||||
(`popoverWillShow`) to reveal items while the panel is open and `resume()`
|
waits ~0.2s for the layout to settle, performs the accessibility action
|
||||||
0.6s after `popoverDidClose`. `suspend`/`resume` do not change the user's
|
(`kAXPressAction`, then `kAXShowMenuAction`), and `resume()`s ~0.1s later. The
|
||||||
`enabled` preference.
|
menu is its own window anchored at the item's on-screen spot, so it survives
|
||||||
|
re-hiding.
|
||||||
|
- **Cannot reveal a single icon.** The spacer hides a contiguous block to its
|
||||||
|
left, so a press can only reveal the whole block. Isolating one item would
|
||||||
|
need either per-item ordering control (not available) or covering the icons
|
||||||
|
with an overlay window instead of moving them off-screen.
|
||||||
- **Order matters**: `AppDelegate` must call `setupStatusItem()` *before*
|
- **Order matters**: `AppDelegate` must call `setupStatusItem()` *before*
|
||||||
`MenuBarHider.shared.prepare()`. If the hider contracts the bar first, the
|
`MenuBarHider.shared.prepare()`. If the hider contracts the bar first, the
|
||||||
hamburger is created into the collapsed space and lands off-screen.
|
hamburger is created into the collapsed space and lands off-screen.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ struct SimpleTopBarApp: App {
|
|||||||
/// full menu bar, a brand new item lands under the notch and is invisible;
|
/// full menu bar, a brand new item lands under the notch and is invisible;
|
||||||
/// writing `NSStatusItem Preferred Position <name>` into defaults puts it in a
|
/// writing `NSStatusItem Preferred Position <name>` into defaults puts it in a
|
||||||
/// visible slot, and the autosave name lets the user's own drag persist.
|
/// 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 statusItem: NSStatusItem?
|
||||||
private var popover: NSPopover?
|
private var popover: NSPopover?
|
||||||
|
|
||||||
@@ -48,6 +48,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|||||||
setupStatusItem()
|
setupStatusItem()
|
||||||
MenuBarHider.shared.prepare()
|
MenuBarHider.shared.prepare()
|
||||||
|
|
||||||
|
if CommandLine.arguments.contains("--diag-press") {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||||
|
MenuBarHider.shared.suspend()
|
||||||
|
}
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.25) {
|
||||||
|
if let item = MenuBarItemManager.enumerate().first(where: {
|
||||||
|
$0.title.lowercased().contains("wireguard")
|
||||||
|
}) {
|
||||||
|
NSLog("diag-press: pressing \(item.appName) after brief reveal")
|
||||||
|
_ = AXUIElementPerformAction(item.element, kAXPressAction as CFString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.45) {
|
||||||
|
MenuBarHider.shared.resume()
|
||||||
|
}
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 4.5) {
|
||||||
|
dumpWindows()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if CommandLine.arguments.contains("--diag") {
|
if CommandLine.arguments.contains("--diag") {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
|
||||||
dumpMenuBarItems()
|
dumpMenuBarItems()
|
||||||
@@ -103,7 +124,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|||||||
let popover = NSPopover()
|
let popover = NSPopover()
|
||||||
popover.behavior = .transient
|
popover.behavior = .transient
|
||||||
popover.animates = false
|
popover.animates = false
|
||||||
popover.delegate = self
|
|
||||||
popover.contentViewController = NSHostingController(
|
popover.contentViewController = NSHostingController(
|
||||||
rootView: TopBarView(
|
rootView: TopBarView(
|
||||||
model: AppListModel.shared,
|
model: AppListModel.shared,
|
||||||
@@ -114,20 +134,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|||||||
self.popover = popover
|
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() {
|
@objc private func togglePopover() {
|
||||||
guard let popover, let button = statusItem?.button else { return }
|
guard let popover, let button = statusItem?.button else { return }
|
||||||
if popover.isShown {
|
if popover.isShown {
|
||||||
|
|||||||
@@ -103,10 +103,31 @@ final class MenuBarItemManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens the real menu bar item by pressing its accessibility element, so
|
/// Opens the real menu bar item, so its menu/popover appears.
|
||||||
/// its menu/popover appears at its true screen location.
|
///
|
||||||
@discardableResult
|
/// A hidden item sits off-screen, and its menu would open off-screen too.
|
||||||
func press(_ item: MenuBarItem) -> Bool {
|
/// 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.2) {
|
||||||
|
_ = self.performPress(item)
|
||||||
|
// The menu is its own window, positioned at the item's on-screen
|
||||||
|
// spot, so the bar can be re-hidden almost immediately.
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
|
hider.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performPress(_ item: MenuBarItem) -> Bool {
|
||||||
if AXUIElementPerformAction(item.element, kAXPressAction as CFString) == .success {
|
if AXUIElementPerformAction(item.element, kAXPressAction as CFString) == .success {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,28 @@
|
|||||||
import AppKit
|
import AppKit
|
||||||
import ApplicationServices
|
import ApplicationServices
|
||||||
|
import CoreGraphics
|
||||||
|
|
||||||
|
/// Diagnostic used by `open SimpleTopBar.app --args --diag-press`. Presses a
|
||||||
|
/// hidden item's accessibility element without revealing it and dumps the
|
||||||
|
/// resulting high-level windows, to see where its menu actually opens.
|
||||||
|
func dumpWindows() {
|
||||||
|
var lines: [String] = []
|
||||||
|
if let list = CGWindowListCopyWindowInfo([], kCGNullWindowID) as? [[String: Any]] {
|
||||||
|
for window in list {
|
||||||
|
let layer = window[kCGWindowLayer as String] as? Int ?? -1
|
||||||
|
guard layer > 20 else { continue }
|
||||||
|
let owner = window[kCGWindowOwnerName as String] as? String ?? "?"
|
||||||
|
let bounds = window[kCGWindowBounds as String] as? [String: Any] ?? [:]
|
||||||
|
let x = bounds["X"] as? Double ?? 0
|
||||||
|
let y = bounds["Y"] as? Double ?? 0
|
||||||
|
let width = bounds["Width"] as? Double ?? 0
|
||||||
|
let height = bounds["Height"] as? Double ?? 0
|
||||||
|
lines.append("\(owner) layer=\(layer) frame=\(Int(x)),\(Int(y)) \(Int(width))x\(Int(height))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try? lines.joined(separator: "\n").write(toFile: "/tmp/simpletopbar-windows.txt", atomically: true, encoding: .utf8)
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
/// Diagnostic used by `open SimpleTopBar.app --args --dump-items`. Writes the
|
/// Diagnostic used by `open SimpleTopBar.app --args --dump-items`. Writes the
|
||||||
/// live Accessibility view of the menu bar to /tmp so it can be inspected
|
/// live Accessibility view of the menu bar to /tmp so it can be inspected
|
||||||
|
|||||||
Reference in New Issue
Block a user