Revert "Hide menu bar icons with a cover instead of moving them off screen"

This reverts commit 3e57efe312.
This commit is contained in:
2026-09-27 10:13:10 +08:00
parent 3e57efe312
commit 569abe967f
6 changed files with 143 additions and 246 deletions
+77 -47
View File
@@ -9,8 +9,6 @@ struct MenuBarItem: Identifiable, Equatable {
let appName: String
let title: String
let icon: NSImage
/// Frame in Accessibility coordinates (origin top-left of the main screen).
let frame: CGRect?
let element: AXUIElement
static func == (lhs: MenuBarItem, rhs: MenuBarItem) -> Bool {
@@ -54,15 +52,9 @@ final class MenuBarItemManager: ObservableObject {
authorized = AXIsProcessTrusted()
guard authorized else {
items = []
MenuBarHider.shared.apply(items: [], avoid: nil)
return
}
let ownPID = ProcessInfo.processInfo.processIdentifier
let all = Self.enumerate(includeOwn: true)
items = all.filter { $0.pid != ownPID }
// Our own hamburger must never be covered, even if it lands inside a run.
let avoid = all.first { $0.pid == ownPID }?.frame
MenuBarHider.shared.apply(items: items, avoid: avoid)
items = Self.enumerate()
}
nonisolated static func enumerate(includeOwn: Bool = false) -> [MenuBarItem] {
@@ -78,11 +70,10 @@ final class MenuBarItemManager: ObservableObject {
var seenTitles = Set<String>()
for (index, child) in children.enumerated() {
let position = pointAttribute(child, kAXPositionAttribute)
// Control Center reports every module it owns, including the
// ones not in the menu bar, which are parked off-screen around
// y=982. Keep only items actually sitting in the bar.
if let position, position.y > 60 {
if let position = pointAttribute(child, kAXPositionAttribute), position.y > 60 {
continue
}
@@ -94,21 +85,12 @@ final class MenuBarItemManager: ObservableObject {
// "Control Center"; show each distinct title once per app.
if !seenTitles.insert(title).inserted { continue }
let size = sizeAttribute(child, kAXSizeAttribute)
let frame: CGRect?
if let position, let size {
frame = CGRect(origin: position, size: size)
} else {
frame = nil
}
found.append(MenuBarItem(
id: "\(pid)-\(index)",
pid: pid,
appName: app.localizedName ?? app.bundleIdentifier ?? "Unknown",
title: title,
icon: Self.icon(forTitle: title, app: app),
frame: frame,
element: child
))
}
@@ -121,11 +103,28 @@ final class MenuBarItemManager: ObservableObject {
}
}
/// Opens the real menu bar item, so its menu/popover appears at its own
/// on-screen position. With the cover approach the item never moves, so no
/// reveal is needed.
/// 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) {
_ = performPress(item)
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 {
@@ -216,26 +215,20 @@ final class MenuBarItemManager: ObservableObject {
guard AXValueGetValue(value as! AXValue, .cgPoint, &point) else { return nil }
return point
}
nonisolated static func sizeAttribute(_ element: AXUIElement, _ attribute: String) -> CGSize? {
var value: CFTypeRef?
guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
let value,
CFGetTypeID(value) == AXValueGetTypeID() else { return nil }
var size = CGSize.zero
guard AXValueGetValue(value as! AXValue, .cgSize, &size) else { return nil }
return size
}
}
/// Menu bar item hiding via a "cover": the icons stay exactly where macOS put
/// them and are hidden by our own frosted window drawn over them. Because they
/// never move, opening one's menu needs no reveal. On by default; the choice is
/// persisted. Always restored on quit.
/// Best-effort menu bar item hiding. A zero-width spacer status item is created
/// at launch; expanding its length to a huge value pushes the other apps' icons
/// off the visible menu bar, so they can only be reached through SimpleTopBar's
/// panel. On by default; the choice is persisted. Always restored on quit, and
/// a crash cleans up automatically because status items die with the process.
@MainActor
final class MenuBarHider: ObservableObject {
static let shared = MenuBarHider()
/// Width that pushes the neighbouring items off the visible menu bar.
static let hiddenLength: CGFloat = 10_000
private static let defaultsKey = "hideMenuBarItems"
@Published private(set) var isHiding = false
@@ -243,27 +236,64 @@ final class MenuBarHider: ObservableObject {
didSet {
guard enabled != oldValue else { return }
UserDefaults.standard.set(enabled, forKey: Self.defaultsKey)
MenuBarItemManager.shared.refresh()
apply(enabled)
}
}
private var spacer: NSStatusItem?
private init() {
// Hide by default: the menu bar then shows only SimpleTopBar's hamburger
// plus the Apple items macOS will not let us cover.
// plus the Apple items macOS will not let us move.
enabled = UserDefaults.standard.object(forKey: Self.defaultsKey) as? Bool ?? true
}
/// Recompute the cover windows for the current menu bar items. `avoid` is
/// our own hamburger's frame, which the cover must never hide.
func apply(items: [MenuBarItem], avoid: CGRect?) {
isHiding = enabled
MenuBarCover.shared.update(enabled: enabled, items: items, avoid: avoid)
/// Create the spacer early (at launch) so it lands as far right as possible.
func prepare() {
guard spacer == nil else { return }
let name = "SimpleTopBarHider"
let key = "NSStatusItem Preferred Position \(name)"
// The boundary between "hidden" and "kept" falls just left of Wi-Fi at
// ~250, so the other apps hide while Apple's Wi-Fi/Clock/Battery/Control
// Center/Spotlight stay visible. `debug.spacerPos` overrides the seed.
if let override = UserDefaults.standard.object(forKey: "debug.spacerPos") as? Double {
UserDefaults.standard.set(override, forKey: key)
} else if UserDefaults.standard.object(forKey: key) == nil {
UserDefaults.standard.set(250.0, forKey: key)
}
let item = NSStatusBar.system.statusItem(withLength: 0)
item.autosaveName = name
item.button?.isHidden = true
spacer = item
apply(enabled)
}
/// Remove the cover. Called on quit. Leaves the user's preference intact.
private func apply(_ hiding: Bool) {
spacer?.length = hiding ? Self.hiddenLength : 0
isHiding = hiding
}
/// Temporarily reveal the items (e.g. while the panel is open) so pressing
/// a tray icon opens its menu at its real, on-screen position. Keeps the
/// user's `enabled` preference untouched.
func suspend() {
guard enabled else { return }
spacer?.length = 0
isHiding = false
}
/// Restore hiding after a `suspend()`.
func resume() {
guard enabled else { return }
spacer?.length = Self.hiddenLength
isHiding = true
}
/// Put the menu bar back the way we found it. Called on quit. Leaves the
/// user's preference intact.
func restore() {
spacer?.length = 0
isHiding = false
MenuBarCover.shared.update(enabled: false, items: [])
}
}