Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

2322 lines
88 KiB
Swift

import AppKit
import ApplicationServices
import Combine
import CoreGraphics
import Foundation
import MacToolsPluginKit
// MARK: - MenuBarHiddenManager
//
// Central coordinator. Owns the divider, item enumerator, event synthesiser
// and icon cache. Three concerns:
//
// 1. `isEnabled` — toggles the divider; permission-free. The hide mechanism
// is purely the expanding NSStatusItem (Thaw / Ice approach).
// 2. Layout drag — moves items between sections by synthesising Cmd+drag
// events. Requires accessibility + screen recording.
// 3. Click forwarding — temporarily moves one hidden item into the visible
// section, clicks it, then returns it after its interface closes.
// Requires accessibility + screen recording.
@MainActor
final class MenuBarHiddenManager: ObservableObject {
@Published private(set) var snapshot: MenuBarHiddenSnapshot = .empty
@Published private(set) var permissions = MenuBarHiddenPermissionsStatus(
hasAccessibility: false,
hasScreenRecording: false
)
let iconCache: MenuBarHiddenIconCache
private let store: MenuBarHiddenStore
private let divider: MenuBarHiddenDivider
private let alwaysHiddenDivider: MenuBarHiddenDivider
private let enumerator: MenuBarHiddenItemEnumerator
private let events: MenuBarHiddenEventSynthesis
private let localization: PluginLocalization
private let permissionProvider: () -> MenuBarHiddenPermissionsStatus
private struct PendingClickRequest {
let item: MenuBarItem
let button: CGMouseButton
}
private var isActive = false
private var settingsVisible = false
private var hiddenIconsPanelVisible = false
private var isDraggingMenuBarItem = false
private var currentDragTarget: MenuBarHiddenMenuBarDragTarget = .unknown
private var isRecoveringControlItemOrder = false
private var shouldRestoreHiddenAfterDrag = false
private var dragSessionID = 0
private var currentDisplayID = CGMainDisplayID()
private var moveTask: Task<Void, Never>?
private var clickTask: Task<Void, Never>?
private var pendingClickRequests: [PendingClickRequest] = []
private var controlItemRecoveryTask: Task<Void, Never>?
private var controlItemRecoveryPollTask: Task<Void, Never>?
private var hiddenRestoreTask: Task<Void, Never>?
private var alwaysHiddenRestoreTask: Task<Void, Never>?
private var layoutRestoreTask: Task<Void, Never>?
private var menuBarDragCommitTask: Task<Void, Never>?
private var menuBarDragCommitSessionID: Int?
private var settingsSettleRefreshTask: Task<Void, Never>?
private var temporaryRehideTask: Task<Void, Never>?
private var temporaryRehideCancellable: AnyCancellable?
private var temporarilyShownItemContexts: [TemporarilyShownItemContext] = []
var hostStatusItemFrameProvider: (() -> NSRect?)?
var resetHostStatusItemPosition: (() -> Void)?
init(
store: MenuBarHiddenStore,
localization: PluginLocalization = PluginLocalization(bundle: .main),
permissionProvider: @escaping () -> MenuBarHiddenPermissionsStatus = {
MenuBarHiddenPermissionsStatus(
hasAccessibility: AXIsProcessTrusted(),
hasScreenRecording: MenuBarHiddenScreenRecordingPermission.isGranted()
)
}
) {
self.store = store
self.divider = MenuBarHiddenDivider(kind: .hidden)
self.alwaysHiddenDivider = MenuBarHiddenDivider(kind: .alwaysHidden)
self.enumerator = MenuBarHiddenItemEnumerator()
self.events = MenuBarHiddenEventSynthesis()
self.iconCache = MenuBarHiddenIconCache()
self.localization = localization
self.permissionProvider = permissionProvider
divider.onFrameChange = { [weak self] in
self?.refresh(reason: .dividerMoved)
}
alwaysHiddenDivider.onFrameChange = { [weak self] in
self?.refresh(reason: .dividerMoved)
}
}
#if DEBUG
func replaceSnapshotForTesting(
visibleItems: [MenuBarItem] = [],
hiddenItems: [MenuBarItem] = [],
alwaysHiddenItems: [MenuBarItem] = [],
permissions: MenuBarHiddenPermissionsStatus = MenuBarHiddenPermissionsStatus(
hasAccessibility: true,
hasScreenRecording: true
)
) {
snapshot = MenuBarHiddenSnapshot(
visibleItems: visibleItems,
hiddenItems: hiddenItems,
alwaysHiddenItems: alwaysHiddenItems,
permissions: permissions
)
}
#endif
// MARK: - Lifecycle
func activate() {
guard !isActive else { return }
isActive = true
refreshPermissions()
if store.isEnabled {
installAndExpand()
} else {
installAndShow()
}
updateAlwaysHiddenDividerVisibility()
rebuildSnapshot()
recoverControlItemOrderIfNeeded(sessionID: nil)
scheduleStoredLayoutRestoreIfNeeded(delay: .milliseconds(700))
scheduleAlwaysHiddenItemsRestoreIfNeeded(delay: .milliseconds(600))
}
func deactivate() {
guard isActive else { return }
isActive = false
moveTask?.cancel(); moveTask = nil
clickTask?.cancel(); clickTask = nil
pendingClickRequests.removeAll()
controlItemRecoveryTask?.cancel(); controlItemRecoveryTask = nil
controlItemRecoveryPollTask?.cancel(); controlItemRecoveryPollTask = nil
hiddenRestoreTask?.cancel(); hiddenRestoreTask = nil
alwaysHiddenRestoreTask?.cancel(); alwaysHiddenRestoreTask = nil
layoutRestoreTask?.cancel(); layoutRestoreTask = nil
cancelMenuBarDragCommit()
settingsSettleRefreshTask?.cancel(); settingsSettleRefreshTask = nil
temporaryRehideTask?.cancel(); temporaryRehideTask = nil
temporaryRehideCancellable?.cancel(); temporaryRehideCancellable = nil
temporarilyShownItemContexts.removeAll()
isDraggingMenuBarItem = false
currentDragTarget = .unknown
isRecoveringControlItemOrder = false
shouldRestoreHiddenAfterDrag = false
invalidateDragSession()
divider.uninstall()
alwaysHiddenDivider.uninstall()
}
// MARK: - Toggle (Function 1, no permissions required)
var isEnabled: Bool {
get { store.isEnabled }
set {
guard store.isEnabled != newValue else { return }
hiddenRestoreTask?.cancel()
hiddenRestoreTask = nil
layoutRestoreTask?.cancel()
layoutRestoreTask = nil
shouldRestoreHiddenAfterDrag = false
store.isEnabled = newValue
if newValue {
installAndExpand()
} else {
if !divider.isInstalled { divider.install() }
divider.showSection(isDragging: false)
}
rebuildSnapshot()
scheduleStoredLayoutRestoreIfNeeded(delay: .milliseconds(500))
}
}
var isAlwaysHiddenEnabled: Bool {
get { store.isAlwaysHiddenEnabled }
set {
guard store.isAlwaysHiddenEnabled != newValue else { return }
alwaysHiddenRestoreTask?.cancel()
alwaysHiddenRestoreTask = nil
guard permissions.canManageItems || !newValue else {
store.isAlwaysHiddenEnabled = false
updateAlwaysHiddenDividerVisibility()
rebuildSnapshot()
refreshIconsIfUIVisible()
return
}
store.isAlwaysHiddenEnabled = newValue
updateAlwaysHiddenDividerVisibility()
rebuildSnapshot()
if newValue {
scheduleAlwaysHiddenItemsRestoreIfNeeded(delay: .milliseconds(500))
}
scheduleStoredLayoutRestoreIfNeeded(delay: .milliseconds(500))
refreshIconsIfUIVisible()
}
}
var showsHiddenIconsInPanel: Bool {
get { store.showsHiddenIconsInPanel }
set {
guard store.showsHiddenIconsInPanel != newValue else { return }
guard permissions.canManageItems || !newValue else {
store.showsHiddenIconsInPanel = false
return
}
store.showsHiddenIconsInPanel = newValue
}
}
var canShowHiddenIconsInPanel: Bool {
permissions.canManageItems && store.showsHiddenIconsInPanel
}
// MARK: - Refresh
func refresh(reason: MenuBarHiddenRefreshReason) {
guard isActive else { return }
MenuBarHiddenLog.plugin.debug("refresh: \(reason.description)")
refreshPermissions()
rebuildSnapshot()
if !isMenuBarDragCommitPending {
scheduleAlwaysHiddenItemsRestoreIfNeeded()
scheduleStoredLayoutRestoreIfNeeded()
}
refreshIconsIfUIVisible()
}
func setSettingsVisible(_ visible: Bool) {
settingsVisible = visible
settingsSettleRefreshTask?.cancel()
settingsSettleRefreshTask = nil
if visible {
refresh(reason: .settingsAppeared)
recoverControlItemOrderIfNeeded(sessionID: nil)
settingsSettleRefreshTask = Task { [weak self] in
// Matches Thaw's settling delay before cache refresh: freshly
// shown NSStatusItem windows can report stale bounds briefly.
try? await Task.sleep(for: .milliseconds(500))
guard !Task.isCancelled else { return }
await MainActor.run {
guard !Task.isCancelled else { return }
self?.refresh(reason: .settingsAppeared)
}
}
}
}
func setHiddenIconsPanelVisible(_ visible: Bool) {
hiddenIconsPanelVisible = visible
if visible { refresh(reason: .hiddenIconsPanelAppeared) }
}
func setDraggingMenuBarItem(_ dragging: Bool, startLocation: NSPoint?) {
guard isDraggingMenuBarItem != dragging else { return }
isDraggingMenuBarItem = dragging
if dragging {
let shouldRestoreHidden = shouldRestoreHiddenAfterDrag || store.isEnabled
invalidateDragSession()
let sessionID = dragSessionID
currentDragTarget = dragTarget(at: startLocation)
cancelMenuBarDragCommit()
hiddenRestoreTask?.cancel()
hiddenRestoreTask = nil
layoutRestoreTask?.cancel()
layoutRestoreTask = nil
controlItemRecoveryTask?.cancel()
controlItemRecoveryTask = nil
isRecoveringControlItemOrder = false
shouldRestoreHiddenAfterDrag = shouldRestoreHidden
if store.isEnabled {
store.isEnabled = false
}
installAndShow()
divider.showSection(isDragging: true)
updateAlwaysHiddenDividerVisibility()
refresh(reason: .dragStarted)
rebuildSnapshot()
startControlItemOrderRecoveryPolling(sessionID: sessionID)
} else {
let sessionID = dragSessionID
stopControlItemOrderRecoveryPolling()
scheduleMenuBarDragCommit(target: currentDragTarget, sessionID: sessionID)
currentDragTarget = .unknown
}
}
// MARK: - Move item (Function 2, requires both permissions)
func moveItem(
id: MenuBarItemTag,
to section: MenuBarHiddenSection,
placement: MenuBarHiddenMovePlacement
) {
guard permissions.canManageItems else {
MenuBarHiddenLog.plugin.debug("moveItem ignored — missing permissions")
return
}
guard let item = snapshot.allItems.first(where: { $0.tag == id }) else { return }
guard !MenuBarHiddenLayoutPolicy.shouldRejectMoveToSection(item: item, section: section) else {
recoverHostIconIfNeeded()
MenuBarHiddenLog.plugin.debug("moveItem ignored — item cannot move to \(section.rawValue)")
return
}
alwaysHiddenRestoreTask?.cancel()
alwaysHiddenRestoreTask = nil
layoutRestoreTask?.cancel()
layoutRestoreTask = nil
moveTask?.cancel()
moveTask = Task { [weak self] in
guard let self else { return }
let moved = await self.performMove(item: item, toSection: section, placement: placement)
guard moved else { return }
self.updateStoredLayoutAfterMove(
for: item,
movedItem: self.snapshotItem(matching: item, in: section),
destination: section
)
}
}
// MARK: - Click forwarding (Function 3, requires both permissions)
func clickItem(_ item: MenuBarItem, button: CGMouseButton) {
guard permissions.canManageItems else { return }
pendingClickRequests.append(PendingClickRequest(item: item, button: button))
startClickProcessingIfNeeded()
}
private func startClickProcessingIfNeeded() {
guard clickTask == nil else { return }
clickTask = Task { [weak self] in
await self?.processPendingClickRequests()
}
}
private func processPendingClickRequests() async {
while !Task.isCancelled {
guard !pendingClickRequests.isEmpty else {
clickTask = nil
return
}
let request = pendingClickRequests.removeFirst()
await performClick(item: request.item, button: request.button)
}
pendingClickRequests.removeAll()
clickTask = nil
}
// MARK: - Permissions
func refreshPermissions() {
let current = currentPermissions()
guard permissions != current else { return }
permissions = current
if !current.canManageItems {
if store.isAlwaysHiddenEnabled {
store.isAlwaysHiddenEnabled = false
updateAlwaysHiddenDividerVisibility()
rebuildSnapshot()
}
if store.showsHiddenIconsInPanel {
store.showsHiddenIconsInPanel = false
}
}
}
func currentPermissions() -> MenuBarHiddenPermissionsStatus {
permissionProvider()
}
// MARK: - Private
private func installAndExpand() {
if !divider.isInstalled { divider.install() }
if visibleControlItemStoredPositionNeedsRecovery() {
divider.showSection(isDragging: false)
scheduleHostIconRecovery(sessionID: nil)
return
}
if isDraggingMenuBarItem {
divider.showSection(isDragging: true)
} else {
divider.hideSection()
}
updateAlwaysHiddenDividerVisibility()
}
private func installAndShow() {
if !divider.isInstalled { divider.install() }
divider.showSection(isDragging: false)
updateAlwaysHiddenDividerVisibility()
}
private func updateAlwaysHiddenDividerVisibility() {
guard isActive else { return }
if store.isAlwaysHiddenEnabled {
if !alwaysHiddenDivider.isInstalled {
alwaysHiddenDivider.install()
}
alwaysHiddenDivider.hideSection()
recoverAlwaysHiddenDividerIfNeeded(sessionID: nil)
} else {
alwaysHiddenDivider.uninstall()
}
}
private func invalidateDragSession() {
dragSessionID &+= 1
}
private func rebuildSnapshot() {
divider.refreshWindowID()
alwaysHiddenDivider.refreshWindowID()
let result = enumerateMenuBarItems(
hiddenDividerWindowID: divider.isInstalled ? divider.windowID : nil,
hiddenDividerFrame: divider.isInstalled ? divider.screenFrame : nil,
alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil,
alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil,
excludedWindowIDs: excludedControlWindowIDs()
)
let allItems = result.items
currentDisplayID = result.displayID
guard let hiddenDividerBounds = result.hiddenDividerBounds else {
snapshot = MenuBarHiddenSnapshot(
visibleItems: [],
hiddenItems: [],
alwaysHiddenItems: [],
permissions: permissions
)
return
}
let layoutItems = MenuBarHiddenLayoutPolicy.layoutEditorItems(from: allItems)
let alwaysHiddenDividerBounds = store.isAlwaysHiddenEnabled ? result.alwaysHiddenDividerBounds : nil
let visibleItems = MenuBarHiddenLayoutPolicy.visibleItems(
from: layoutItems,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: alwaysHiddenDividerBounds
)
let hiddenItems = MenuBarHiddenLayoutPolicy.hiddenItems(
from: layoutItems,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: alwaysHiddenDividerBounds
)
let alwaysHiddenItems = MenuBarHiddenLayoutPolicy.alwaysHiddenItems(
from: layoutItems,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: alwaysHiddenDividerBounds
)
snapshot = MenuBarHiddenSnapshot(
visibleItems: visibleItems,
hiddenItems: hiddenItems,
alwaysHiddenItems: alwaysHiddenItems,
permissions: permissions
)
}
private func refreshIconsIfUIVisible() {
guard settingsVisible || hiddenIconsPanelVisible else { return }
guard permissions.canManageItems else { return }
iconCache.refresh(
groups: [snapshot.visibleItems, snapshot.hiddenItems, snapshot.alwaysHiddenItems],
displayID: currentDisplayID
)
}
private func excludedControlWindowIDs() -> Set<CGWindowID> {
var ids = Set<CGWindowID>()
if divider.isInstalled {
ids.formUnion(divider.hiddenControlWindowIDs)
}
if alwaysHiddenDivider.isInstalled {
ids.formUnion(alwaysHiddenDivider.hiddenControlWindowIDs)
}
return ids
}
private func enumerateMenuBarItems(
hiddenDividerWindowID: CGWindowID?,
hiddenDividerFrame: NSRect?,
alwaysHiddenDividerWindowID: CGWindowID?,
alwaysHiddenDividerFrame: NSRect?,
excludedWindowIDs: Set<CGWindowID>
) -> MenuBarHiddenEnumeratorResult {
enumerator.enumerate(
hiddenDividerWindowID: hiddenDividerWindowID,
hiddenDividerFrame: hiddenDividerFrame,
alwaysHiddenDividerWindowID: alwaysHiddenDividerWindowID,
alwaysHiddenDividerFrame: alwaysHiddenDividerFrame,
excludedWindowIDs: excludedWindowIDs,
canResolveSourcePIDs: MenuBarHiddenSourcePIDResolutionPolicy.canResolve(
permissions: permissions
)
)
}
private func localizedDescription(for error: Error) -> String {
if let eventError = error as? MenuBarHiddenEventError {
return eventError.localizedDescription(localization: localization)
}
return error.localizedDescription
}
// MARK: - Move
private func performMove(
item: MenuBarItem,
toSection: MenuBarHiddenSection,
placement: MenuBarHiddenMovePlacement
) async -> Bool {
await events.waitForUserInputPause()
let live = enumerateMenuBarItems(
hiddenDividerWindowID: divider.windowID,
hiddenDividerFrame: divider.screenFrame,
alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil,
alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil,
excludedWindowIDs: excludedControlWindowIDs()
)
guard
let liveItem = live.items.first(where: { $0.tag == item.tag }),
let target = computeMoveTarget(
section: toSection,
placement: placement,
items: live.items,
hiddenDividerBounds: live.hiddenDividerBounds,
alwaysHiddenDividerBounds: live.alwaysHiddenDividerBounds
)
else {
MenuBarHiddenLog.plugin.warning("performMove: could not resolve live item or target X")
return false
}
do {
try await events.move(item: liveItem, to: target)
} catch {
MenuBarHiddenLog.plugin.error("performMove failed: \(self.localizedDescription(for: error))")
return false
}
try? await Task.sleep(for: .milliseconds(300))
rebuildSnapshot()
recoverHostIconIfNeeded()
refreshIconsIfUIVisible()
return snapshotItem(matching: item, in: toSection) != nil
}
private func snapshotItem(matching item: MenuBarItem, in section: MenuBarHiddenSection) -> MenuBarItem? {
let items: [MenuBarItem]
switch section {
case .visible:
items = snapshot.visibleItems
case .hidden:
items = snapshot.hiddenItems
case .alwaysHidden:
items = snapshot.alwaysHiddenItems
}
return items.first {
$0.windowID == item.windowID
} ?? items.first {
$0.tag.matchesIgnoringWindowID(item.tag)
&& ($0.sourcePID ?? $0.ownerPID) == (item.sourcePID ?? item.ownerPID)
}
}
private func updateStoredLayoutAfterMove(
for originalItem: MenuBarItem,
movedItem: MenuBarItem?,
destination: MenuBarHiddenSection
) {
switch destination {
case .alwaysHidden:
store.recordAlwaysHiddenItem((movedItem ?? originalItem).tag)
case .visible, .hidden:
if store.isAlwaysHiddenEnabled {
store.removeAlwaysHiddenItem(originalItem.tag)
if let movedItem {
store.removeAlwaysHiddenItem(movedItem.tag)
}
}
}
recordCurrentVisibleHiddenLayout()
scheduleStoredLayoutRestoreIfNeeded(delay: .milliseconds(500))
}
private func prepareStoredVisibleHiddenLayoutIfNeeded() {
guard permissions.canManageItems else { return }
guard !snapshot.allItems.isEmpty else { return }
guard !isDraggingMenuBarItem, !isRecoveringControlItemOrder else { return }
guard temporarilyShownItemContexts.isEmpty else { return }
if store.hasVisibleHiddenLayout {
appendNewItemsToStoredVisibleHiddenLayout()
} else {
recordCurrentVisibleHiddenLayout()
}
}
private func appendNewItemsToStoredVisibleHiddenLayout() {
let layout = storedLayout()
let next = MenuBarHiddenStoredLayoutPolicy.appendNewItemsToHiddenByDefault(
items: snapshot.allItems,
storedLayout: layout
)
guard next.visibleItemStableKeys != store.visibleItemStableKeys
|| next.hiddenItemStableKeys != store.hiddenItemStableKeys
else {
return
}
store.recordVisibleHiddenLayout(
visibleKeys: next.visibleItemStableKeys,
hiddenKeys: next.hiddenItemStableKeys
)
}
private func recordCurrentVisibleHiddenLayout() {
let next = MenuBarHiddenStoredLayoutPolicy.visibleHiddenLayout(
visibleItems: snapshot.visibleItems,
hiddenItems: snapshot.hiddenItems,
alwaysHiddenItems: snapshot.alwaysHiddenItems,
previousVisibleItemStableKeys: store.visibleItemStableKeys,
previousHiddenItemStableKeys: store.hiddenItemStableKeys
)
store.recordVisibleHiddenLayout(
visibleKeys: next.visibleItemStableKeys,
hiddenKeys: next.hiddenItemStableKeys
)
}
private func storedLayout() -> MenuBarHiddenStoredLayout {
MenuBarHiddenStoredLayout(
visibleItemStableKeys: store.visibleItemStableKeys,
hiddenItemStableKeys: store.hiddenItemStableKeys,
alwaysHiddenItemStableKeys: store.alwaysHiddenItemStableKeys,
isAlwaysHiddenEnabled: store.isAlwaysHiddenEnabled
)
}
private func scheduleStoredLayoutRestoreIfNeeded(delay: Duration = .milliseconds(300)) {
prepareStoredVisibleHiddenLayoutIfNeeded()
guard canRestoreStoredVisibleHiddenLayout else {
layoutRestoreTask?.cancel()
layoutRestoreTask = nil
return
}
guard !storedVisibleHiddenRestoreCandidates().isEmpty else {
layoutRestoreTask?.cancel()
layoutRestoreTask = nil
return
}
layoutRestoreTask?.cancel()
layoutRestoreTask = Task { [weak self] in
try? await Task.sleep(for: delay)
guard !Task.isCancelled else { return }
guard let self else { return }
await self.restoreStoredVisibleHiddenLayout()
}
}
private func restoreStoredVisibleHiddenLayout() async {
guard canRestoreStoredVisibleHiddenLayout else {
layoutRestoreTask = nil
return
}
let candidates = storedVisibleHiddenRestoreCandidates()
guard !candidates.isEmpty else {
layoutRestoreTask = nil
return
}
var restoredCount = 0
for candidate in candidates {
guard !Task.isCancelled else {
layoutRestoreTask = nil
return
}
guard canRestoreStoredVisibleHiddenLayout else {
scheduleStoredLayoutRestoreIfNeeded(delay: .milliseconds(600))
return
}
let placement = storedVisibleHiddenPlacement(
for: candidate.item,
in: candidate.section
)
if await performMove(item: candidate.item, toSection: candidate.section, placement: placement) {
restoredCount += 1
}
try? await Task.sleep(for: .milliseconds(100))
}
layoutRestoreTask = nil
if restoredCount > 0, !storedVisibleHiddenRestoreCandidates().isEmpty {
scheduleStoredLayoutRestoreIfNeeded(delay: .milliseconds(900))
}
}
private var canRestoreStoredVisibleHiddenLayout: Bool {
isActive
&& store.hasVisibleHiddenLayout
&& permissions.canManageItems
&& divider.isInstalled
&& !isDraggingMenuBarItem
&& !isRecoveringControlItemOrder
&& temporarilyShownItemContexts.isEmpty
}
private struct StoredVisibleHiddenRestoreCandidate {
let item: MenuBarItem
let section: MenuBarHiddenSection
}
private func storedVisibleHiddenRestoreCandidates() -> [StoredVisibleHiddenRestoreCandidate] {
let layout = storedLayout()
let currentSections = currentVisibleHiddenSectionsByStableKey()
let keyedItems = Dictionary(grouping: snapshot.allItems, by: { $0.tag.stableKey })
.compactMapValues(\.first)
return (layout.hiddenItemStableKeys + layout.visibleItemStableKeys).compactMap { key in
guard let item = keyedItems[key] else { return nil }
let desiredSection = MenuBarHiddenStoredLayoutPolicy.desiredSection(
for: item,
storedLayout: layout
)
guard desiredSection != .alwaysHidden else { return nil }
guard currentSections[key] != desiredSection else { return nil }
guard !MenuBarHiddenLayoutPolicy.shouldRejectMoveToSection(item: item, section: desiredSection) else {
return nil
}
return StoredVisibleHiddenRestoreCandidate(item: item, section: desiredSection)
}
}
private func currentVisibleHiddenSectionsByStableKey() -> [String: MenuBarHiddenSection] {
var sections: [String: MenuBarHiddenSection] = [:]
for item in snapshot.visibleItems {
sections[item.tag.stableKey] = .visible
}
for item in snapshot.hiddenItems {
sections[item.tag.stableKey] = .hidden
}
for item in snapshot.alwaysHiddenItems {
sections[item.tag.stableKey] = .alwaysHidden
}
return sections
}
private func storedVisibleHiddenPlacement(
for item: MenuBarItem,
in section: MenuBarHiddenSection
) -> MenuBarHiddenMovePlacement {
let sectionKeys: [String]
let sectionItems: [MenuBarItem]
switch section {
case .visible:
sectionKeys = store.visibleItemStableKeys
sectionItems = snapshot.visibleItems
case .hidden:
sectionKeys = store.hiddenItemStableKeys
sectionItems = snapshot.hiddenItems
case .alwaysHidden:
return .end
}
guard let index = sectionKeys.firstIndex(of: item.tag.stableKey) else {
return .end
}
let itemsByKey = Dictionary(grouping: sectionItems, by: { $0.tag.stableKey })
.compactMapValues(\.first)
for nextKey in sectionKeys.dropFirst(index + 1) {
if let nextItem = itemsByKey[nextKey] {
return .before(nextItem.tag)
}
}
for previousKey in sectionKeys[..<index].reversed() {
if let previousItem = itemsByKey[previousKey] {
return .after(previousItem.tag)
}
}
return .end
}
private func scheduleAlwaysHiddenItemsRestoreIfNeeded(delay: Duration = .milliseconds(250)) {
guard canRestoreAlwaysHiddenItems else {
alwaysHiddenRestoreTask?.cancel()
alwaysHiddenRestoreTask = nil
return
}
guard !alwaysHiddenRestoreCandidates().isEmpty else { return }
alwaysHiddenRestoreTask?.cancel()
alwaysHiddenRestoreTask = Task { [weak self] in
try? await Task.sleep(for: delay)
guard !Task.isCancelled else { return }
guard let self else { return }
await self.restoreRecordedAlwaysHiddenItems()
}
}
private func restoreRecordedAlwaysHiddenItems() async {
guard canRestoreAlwaysHiddenItems else {
alwaysHiddenRestoreTask = nil
return
}
let candidates = alwaysHiddenRestoreCandidates()
guard !candidates.isEmpty else {
alwaysHiddenRestoreTask = nil
return
}
var restoredCount = 0
for item in candidates {
guard !Task.isCancelled else { return }
guard canRestoreAlwaysHiddenItems else {
scheduleAlwaysHiddenItemsRestoreIfNeeded(delay: .milliseconds(500))
return
}
if await performMove(item: item, toSection: .alwaysHidden, placement: .end) {
restoredCount += 1
}
try? await Task.sleep(for: .milliseconds(100))
}
alwaysHiddenRestoreTask = nil
if restoredCount > 0, !alwaysHiddenRestoreCandidates().isEmpty {
scheduleAlwaysHiddenItemsRestoreIfNeeded(delay: .milliseconds(750))
}
}
private var canRestoreAlwaysHiddenItems: Bool {
isActive
&& store.isAlwaysHiddenEnabled
&& permissions.canManageItems
&& alwaysHiddenDivider.isInstalled
&& !isDraggingMenuBarItem
&& !isRecoveringControlItemOrder
&& temporarilyShownItemContexts.isEmpty
}
private func alwaysHiddenRestoreCandidates() -> [MenuBarItem] {
MenuBarHiddenAlwaysHiddenRestorePolicy.restoreCandidates(
snapshot: snapshot,
recordedStableKeys: store.alwaysHiddenItemStableKeys
)
}
private func computeMoveTarget(
section: MenuBarHiddenSection,
placement: MenuBarHiddenMovePlacement,
items: [MenuBarItem],
hiddenDividerBounds: CGRect?,
alwaysHiddenDividerBounds: CGRect?
) -> MenuBarHiddenResolvedMoveTarget? {
guard let hiddenDivider = hiddenDividerBounds else { return nil }
func target(_ item: MenuBarItem, x: CGFloat) -> MenuBarHiddenResolvedMoveTarget {
MenuBarHiddenResolvedMoveTarget(
point: CGPoint(x: x, y: item.bounds.minY),
windowID: item.windowID
)
}
func target(x: CGFloat, y: CGFloat, windowID: CGWindowID?) -> MenuBarHiddenResolvedMoveTarget {
MenuBarHiddenResolvedMoveTarget(
point: CGPoint(x: x, y: y),
windowID: windowID
)
}
switch (section, placement) {
case (.hidden, .end):
if let alwaysHiddenDividerBounds {
return target(
x: alwaysHiddenDividerBounds.maxX,
y: alwaysHiddenDividerBounds.minY,
windowID: alwaysHiddenDivider.windowID
)
}
return target(x: hiddenDivider.minX, y: hiddenDivider.minY, windowID: divider.windowID)
case (.hidden, .before(let tag)):
guard let item = items.first(where: { $0.tag == tag }) else { return nil }
return target(item, x: item.bounds.minX)
case (.hidden, .after(let tag)):
guard let item = items.first(where: { $0.tag == tag }) else { return nil }
return target(item, x: item.bounds.maxX)
case (.alwaysHidden, .end):
if let alwaysHiddenDividerBounds {
return target(
x: alwaysHiddenDividerBounds.minX,
y: alwaysHiddenDividerBounds.minY,
windowID: alwaysHiddenDivider.windowID
)
}
return target(x: hiddenDivider.minX, y: hiddenDivider.minY, windowID: divider.windowID)
case (.alwaysHidden, .before(let tag)):
guard let item = items.first(where: { $0.tag == tag }) else { return nil }
return target(item, x: item.bounds.minX)
case (.alwaysHidden, .after(let tag)):
guard let item = items.first(where: { $0.tag == tag }) else { return nil }
return target(item, x: item.bounds.maxX)
case (.visible, .end):
return target(x: hiddenDivider.maxX, y: hiddenDivider.minY, windowID: divider.windowID)
case (.visible, .before(let tag)):
guard let item = items.first(where: { $0.tag == tag }) else { return nil }
return target(item, x: item.bounds.minX)
case (.visible, .after(let tag)):
guard let item = items.first(where: { $0.tag == tag }) else { return nil }
return target(item, x: item.bounds.maxX)
}
}
// MARK: - Click forwarding
private final class TemporarilyShownItemContext {
let identity: MenuBarHiddenTemporaryItemIdentity
let ownerPID: pid_t
let returnDestination: MenuBarHiddenReturnDestination
let primaryReturnAnchor: MenuBarHiddenReturnAnchor?
let fallbackReturnAnchor: MenuBarHiddenReturnAnchor?
let interfaceOwner: MenuBarItem
var interfaceWindowID: CGWindowID?
private var resolvedPopupDetectionPID: pid_t
let firstShownDate = Date()
var interfaceDiscoveryDeadline: Date {
firstShownDate.addingTimeInterval(1)
}
var popupDetectionPID: pid_t {
resolvedPopupDetectionPID
}
var rehideAttempts = 0
var notFoundAttempts = 0
var nextReturnAttemptDate = Date.distantPast
init(
item: MenuBarItem,
returnDestination: MenuBarHiddenReturnDestination,
primaryReturnAnchor: MenuBarHiddenReturnAnchor?,
fallbackReturnAnchor: MenuBarHiddenReturnAnchor?
) {
self.identity = MenuBarHiddenTemporaryItemIdentity(item: item)
self.ownerPID = item.ownerPID
self.interfaceOwner = item
self.returnDestination = returnDestination
self.primaryReturnAnchor = primaryReturnAnchor
self.fallbackReturnAnchor = fallbackReturnAnchor
self.resolvedPopupDetectionPID = MenuBarHiddenTemporaryInterfacePolicy.popupDetectionPID(
sourcePID: item.sourcePID,
ownerPID: item.ownerPID
)
}
func updatePopupDetectionPID(from item: MenuBarItem) {
guard let sourcePID = item.sourcePID else { return }
resolvedPopupDetectionPID = sourcePID
}
func scheduleReturnRetry(after delay: TimeInterval) {
nextReturnAttemptDate = Date().addingTimeInterval(delay)
}
}
private enum TemporaryRehideResult {
case returned
case notFound
case retryLater
}
private enum TemporaryRehideReadiness {
case ready
case waitingForInterface
case waitingForUserInput
case waitingForRetry
}
private struct LiveClickResolution {
let result: MenuBarHiddenEnumeratorResult
let layoutItems: [MenuBarItem]
let item: MenuBarItem
}
private func performClick(item: MenuBarItem, button: CGMouseButton) async {
var shouldRunTemporaryRehideLoopOnExit = false
defer {
if shouldRunTemporaryRehideLoopOnExit, !temporarilyShownItemContexts.isEmpty {
startTemporaryRehideLoop()
}
}
guard var resolution = liveClickResolution(matching: item) else {
MenuBarHiddenLog.plugin.warning("performClick: missing live item \(item.tag.stableKey)")
return
}
if resolution.item.isOnScreen {
shouldRunTemporaryRehideLoopOnExit = await clickOnScreenItem(
resolution.item,
button: button
)
return
}
if !temporarilyShownItemContexts.isEmpty {
guard await prepareForTemporaryShow(replacingExistingContextsUsing: resolution.result) else {
MenuBarHiddenLog.plugin.warning(
"performClick: previous temporary item is still pending; skipping temporary show for \(item.tag.stableKey)"
)
return
}
guard let refreshedResolution = liveClickResolution(matching: item) else {
MenuBarHiddenLog.plugin.warning("performClick: missing live item after temporary cleanup \(item.tag.stableKey)")
return
}
resolution = refreshedResolution
if resolution.item.isOnScreen {
shouldRunTemporaryRehideLoopOnExit = await clickOnScreenItem(
resolution.item,
button: button
)
return
}
}
guard
let hiddenDividerBounds = resolution.result.hiddenDividerBounds,
let originalSection = originalSection(
for: resolution.item,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: resolution.result.alwaysHiddenDividerBounds
),
let returnDestination = returnDestination(
for: resolution.item,
section: originalSection,
items: resolution.layoutItems,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: resolution.result.alwaysHiddenDividerBounds
),
let showTarget = temporaryShowTarget(
items: resolution.result.items,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: resolution.result.alwaysHiddenDividerBounds
)
else {
MenuBarHiddenLog.plugin.warning("performClick: could not resolve temporary show target")
return
}
let temporaryContext = TemporarilyShownItemContext(
item: resolution.item,
returnDestination: returnDestination.destination,
primaryReturnAnchor: returnDestination.primaryAnchor,
fallbackReturnAnchor: returnDestination.fallbackAnchor
)
do {
try await events.move(item: resolution.item, to: showTarget)
} catch {
MenuBarHiddenLog.plugin.error("temporary show failed: \(self.localizedDescription(for: error))")
return
}
temporarilyShownItemContexts = [temporaryContext]
shouldRunTemporaryRehideLoopOnExit = true
let clickItem = await refreshedClickItem(
matching: resolution.item,
previousBounds: resolution.item.bounds
)
temporaryContext.updatePopupDetectionPID(from: clickItem)
try? await Task.sleep(for: .milliseconds(25))
guard MenuBarHiddenWindowServer.isWindowOnScreen(clickItem.windowID) else {
MenuBarHiddenLog.plugin.warning("performClick: temporary show did not make item visible \(resolution.item.tag.stableKey)")
return
}
let baselineWindowIDs = Set(MenuBarHiddenWindowServer.onScreenWindowIDs())
do {
try await events.click(item: clickItem, button: button)
temporaryContext.interfaceWindowID = await openedTemporaryInterfaceWindowID(
afterClickBy: clickItem,
baselineWindowIDs: baselineWindowIDs
)
} catch {
MenuBarHiddenLog.plugin.error("click failed: \(self.localizedDescription(for: error))")
}
rebuildSnapshot()
refreshIconsIfUIVisible()
}
private func liveClickResolution(matching item: MenuBarItem) -> LiveClickResolution? {
let result = enumerateMenuBarItems(
hiddenDividerWindowID: divider.windowID,
hiddenDividerFrame: divider.screenFrame,
alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil,
alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil,
excludedWindowIDs: excludedControlWindowIDs()
)
let layoutItems = MenuBarHiddenLayoutPolicy.layoutEditorItems(from: result.items)
guard let liveItem = resolvedLiveItem(matching: item, in: layoutItems) else {
return nil
}
return LiveClickResolution(
result: result,
layoutItems: layoutItems,
item: liveItem
)
}
private func clickOnScreenItem(_ item: MenuBarItem, button: CGMouseButton) async -> Bool {
let baselineWindowIDs = Set(MenuBarHiddenWindowServer.onScreenWindowIDs())
do {
try await events.click(item: item, button: button)
guard let context = temporarilyShownItemContexts.first(where: {
contextMatchesItem($0, item: item)
}) else {
return false
}
context.updatePopupDetectionPID(from: item)
context.interfaceWindowID = await openedTemporaryInterfaceWindowID(
afterClickBy: item,
baselineWindowIDs: baselineWindowIDs
)
return true
} catch {
MenuBarHiddenLog.plugin.error("click failed: \(self.localizedDescription(for: error))")
return false
}
}
private func prepareForTemporaryShow(
replacingExistingContextsUsing result: MenuBarHiddenEnumeratorResult
) async -> Bool {
guard !temporarilyShownItemContexts.isEmpty else { return true }
stopTemporaryRehideLoop()
discardReturnedOrMissingTemporaryContexts(using: result)
guard !temporarilyShownItemContexts.isEmpty else { return true }
await rehideTemporarilyShownItems(
force: true,
isCalledFromTemporarilyShow: true,
discardsMissingItems: true
)
return temporarilyShownItemContexts.isEmpty
}
private func discardReturnedOrMissingTemporaryContexts(using result: MenuBarHiddenEnumeratorResult) {
guard !temporarilyShownItemContexts.isEmpty else { return }
guard let hiddenDividerBounds = result.hiddenDividerBounds else { return }
temporarilyShownItemContexts.removeAll { context in
guard let liveItem = result.items.first(where: {
contextMatchesItem(context, item: $0)
}) else {
return !temporaryContextWindowIsVisible(context)
}
return MenuBarHiddenLayoutPolicy.section(
for: liveItem,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: result.alwaysHiddenDividerBounds
) == context.returnDestination.section
}
}
private func resolvedLiveItem(matching item: MenuBarItem, in items: [MenuBarItem]) -> MenuBarItem? {
if let exact = items.first(where: { $0.windowID == item.windowID }) {
return exact
}
let sourcePID = item.sourcePID ?? item.ownerPID
return items.first {
$0.tag.matchesIgnoringWindowID(item.tag)
&& ($0.sourcePID ?? $0.ownerPID) == sourcePID
}
}
private func contextMatchesItem(
_ context: TemporarilyShownItemContext,
item: MenuBarItem
) -> Bool {
if context.identity.matches(item) {
return true
}
return item.tag.matchesIgnoringWindowID(context.identity.tag)
&& (item.sourcePID ?? item.ownerPID) == context.popupDetectionPID
}
private func originalSection(
for item: MenuBarItem,
hiddenDividerBounds: CGRect,
alwaysHiddenDividerBounds: CGRect?
) -> MenuBarHiddenSection? {
let section = MenuBarHiddenLayoutPolicy.section(
for: item,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: alwaysHiddenDividerBounds
)
return section == .visible ? nil : section
}
private func returnDestination(
for item: MenuBarItem,
section: MenuBarHiddenSection,
items: [MenuBarItem],
hiddenDividerBounds: CGRect,
alwaysHiddenDividerBounds: CGRect?
) -> (
destination: MenuBarHiddenReturnDestination,
primaryAnchor: MenuBarHiddenReturnAnchor?,
fallbackAnchor: MenuBarHiddenReturnAnchor?
)? {
let sectionItems = MenuBarHiddenLayoutPolicy.sectionItems(
section,
from: items,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: alwaysHiddenDividerBounds
)
guard let destination = MenuBarHiddenLayoutPolicy.returnDestination(
for: item,
in: sectionItems,
section: section
) else {
return nil
}
let primaryAnchor = anchor(for: destination.placement, in: sectionItems)
let fallbackAnchor = destination.fallbackPlacement.flatMap { anchor(for: $0, in: sectionItems) }
return (destination, primaryAnchor, fallbackAnchor)
}
private func anchor(
for placement: MenuBarHiddenMovePlacement,
in items: [MenuBarItem]
) -> MenuBarHiddenReturnAnchor? {
switch placement {
case .end:
return nil
case .before(let tag), .after(let tag):
guard let item = items.first(where: { $0.tag.matchesIgnoringWindowID(tag) }) else {
return nil
}
return MenuBarHiddenReturnAnchor(tag: item.tag, sourcePID: item.sourcePID ?? item.ownerPID)
}
}
private func temporaryShowTarget(
items: [MenuBarItem],
hiddenDividerBounds: CGRect,
alwaysHiddenDividerBounds: CGRect?
) -> MenuBarHiddenResolvedMoveTarget? {
let visibleAnchor = items.first(where: \.isVisibleControlItem)
?? items.first(where: \.isHostApplicationIcon)
if let visibleAnchor,
let target = computeMoveTarget(
section: .visible,
placement: .before(visibleAnchor.tag),
items: items,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: alwaysHiddenDividerBounds
)
{
return target
}
return computeMoveTarget(
section: .visible,
placement: .end,
items: items,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: alwaysHiddenDividerBounds
)
}
private func refreshedClickItem(
matching item: MenuBarItem,
previousBounds: CGRect?
) async -> MenuBarItem {
await waitForItemToLeavePreviousBounds(item, previousBounds: previousBounds)
await waitForItemPositionToSettle(item)
let result = enumerateMenuBarItems(
hiddenDividerWindowID: divider.windowID,
hiddenDividerFrame: divider.screenFrame,
alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil,
alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil,
excludedWindowIDs: excludedControlWindowIDs()
)
let sourcePID = item.sourcePID ?? item.ownerPID
return result.items.first {
$0.windowID == item.windowID && $0.isOnScreen
} ?? result.items.first {
$0.tag.matchesIgnoringWindowID(item.tag)
&& ($0.sourcePID ?? $0.ownerPID) == sourcePID
&& $0.isOnScreen
} ?? item
}
private func waitForItemToLeavePreviousBounds(_ item: MenuBarItem, previousBounds: CGRect?) async {
guard let previousBounds else {
try? await Task.sleep(for: .milliseconds(150))
return
}
let deadline = Date().addingTimeInterval(0.15)
while Date() < deadline {
if let bounds = MenuBarHiddenWindowServer.screenRect(for: item.windowID),
bounds.origin != previousBounds.origin
{
return
}
try? await Task.sleep(for: .milliseconds(15))
}
}
private func waitForItemPositionToSettle(_ item: MenuBarItem) async {
let deadline = Date().addingTimeInterval(0.25)
var previousBounds = MenuBarHiddenWindowServer.screenRect(for: item.windowID)
var stableReads = 0
while Date() < deadline {
try? await Task.sleep(for: .milliseconds(20))
guard let currentBounds = MenuBarHiddenWindowServer.screenRect(for: item.windowID) else {
previousBounds = nil
stableReads = 0
continue
}
if currentBounds == previousBounds {
stableReads += 1
if stableReads >= 2 {
return
}
} else {
stableReads = 0
}
previousBounds = currentBounds
}
}
private func openedTemporaryInterfaceWindowID(
afterClickBy item: MenuBarItem,
baselineWindowIDs: Set<CGWindowID>
) async -> CGWindowID? {
let deadline = Date().addingTimeInterval(0.7)
while Date() < deadline {
let windowsAfterClick = WindowInfo.createOnScreenWindows()
if let window = windowsAfterClick.first(where: {
MenuBarHiddenTemporaryInterfacePolicy.isNewTemporaryInterfaceWindow(
$0,
item: item,
contextWindowID: item.windowID,
baselineWindowIDs: baselineWindowIDs
)
}) {
return window.windowID
}
try? await Task.sleep(for: .milliseconds(40))
}
return nil
}
private func startTemporaryRehideLoop() {
guard !temporarilyShownItemContexts.isEmpty else { return }
temporaryRehideTask?.cancel()
temporaryRehideTask = Task { [weak self] in
while !Task.isCancelled {
try? await Task.sleep(for: .milliseconds(250))
guard !Task.isCancelled else { return }
guard let self else { return }
let isFinished = await self.rehideTemporarilyShownItemsIfReady()
if isFinished { return }
}
}
temporaryRehideCancellable?.cancel()
temporaryRehideCancellable = NSWorkspace.shared.publisher(for: \.frontmostApplication)
.dropFirst()
.debounce(for: .milliseconds(200), scheduler: DispatchQueue.main)
.sink { [weak self] _ in
guard let self else { return }
Task { [weak self] in
guard let self else { return }
_ = await self.rehideTemporarilyShownItemsIfReady()
}
}
}
private func stopTemporaryRehideLoop() {
temporaryRehideTask?.cancel()
temporaryRehideTask = nil
temporaryRehideCancellable?.cancel()
temporaryRehideCancellable = nil
}
private func rehideTemporarilyShownItemsIfReady() async -> Bool {
guard !temporarilyShownItemContexts.isEmpty else {
stopTemporaryRehideLoop()
return true
}
guard temporaryRehideReadiness() == .ready else {
return false
}
await rehideTemporarilyShownItems()
return temporarilyShownItemContexts.isEmpty
}
private func rehideTemporarilyShownItems(
force: Bool = false,
isCalledFromTemporarilyShow: Bool = false,
discardsMissingItems: Bool = false
) async {
guard !temporarilyShownItemContexts.isEmpty else { return }
if !force {
guard temporaryRehideReadiness() == .ready else {
startTemporaryRehideLoop()
return
}
}
var pending = temporarilyShownItemContexts
temporarilyShownItemContexts.removeAll()
var failed: [TemporarilyShownItemContext] = []
try? await Task.sleep(
for: isCalledFromTemporarilyShow ? .milliseconds(50) : .milliseconds(250)
)
while let context = pending.popLast() {
switch await returnTemporarilyShownItem(context) {
case .returned:
continue
case .notFound:
if discardsMissingItems, !temporaryContextWindowIsVisible(context) {
continue
}
context.scheduleReturnRetry(after: min(5, 0.5 * Double(max(1, context.notFoundAttempts))))
failed.append(context)
case .retryLater:
if context.rehideAttempts < 3 {
pending.append(context)
} else {
context.rehideAttempts = 0
context.scheduleReturnRetry(after: 1)
failed.append(context)
}
}
}
temporarilyShownItemContexts = failed.reversed()
if temporarilyShownItemContexts.isEmpty {
stopTemporaryRehideLoop()
updateAlwaysHiddenDividerVisibility()
try? await Task.sleep(for: .milliseconds(200))
rebuildSnapshot()
scheduleAlwaysHiddenItemsRestoreIfNeeded()
scheduleStoredLayoutRestoreIfNeeded()
refreshIconsIfUIVisible()
} else {
startTemporaryRehideLoop()
}
}
private func temporaryContextWindowIsVisible(_ context: TemporarilyShownItemContext) -> Bool {
MenuBarHiddenWindowServer.isWindowOnScreen(context.identity.windowID)
}
private func hasUserPausedInput() -> Bool {
NSEvent.modifierFlags.isEmpty
&& !MouseCursorHelper.lastMovementOccurred(within: 0.25)
&& !MouseCursorHelper.lastScrollWheelOccurred(within: 0.25)
&& !MouseCursorHelper.isButtonPressed()
}
private func temporaryRehideReadiness() -> TemporaryRehideReadiness {
if temporarilyShownItemContexts.contains(where: isTemporaryItemInterfaceActive) {
return .waitingForInterface
}
if temporarilyShownItemContexts.contains(where: { $0.nextReturnAttemptDate > Date() }) {
return .waitingForRetry
}
if !hasUserPausedInput() {
return .waitingForUserInput
}
return .ready
}
private func isTemporaryItemInterfaceActive(_ context: TemporarilyShownItemContext) -> Bool {
if let interfaceWindowID = context.interfaceWindowID {
return isTemporaryInterfaceWindowActive(interfaceWindowID, item: context.interfaceOwner)
}
if Date() < context.interfaceDiscoveryDeadline {
return true
}
return false
}
private func isTemporaryInterfaceWindowActive(_ windowID: CGWindowID, item: MenuBarItem) -> Bool {
guard let window = WindowInfo(windowID: windowID) else {
return false
}
return MenuBarHiddenTemporaryInterfacePolicy.isTemporaryInterfaceWindow(window, for: item)
}
private func returnTemporarilyShownItem(_ context: TemporarilyShownItemContext) async -> TemporaryRehideResult {
let result = enumerateMenuBarItems(
hiddenDividerWindowID: divider.windowID,
hiddenDividerFrame: divider.screenFrame,
alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil,
alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil,
excludedWindowIDs: excludedControlWindowIDs()
)
guard let hiddenDividerBounds = result.hiddenDividerBounds else {
context.rehideAttempts += 1
return .retryLater
}
let layoutItems = MenuBarHiddenLayoutPolicy.layoutEditorItems(from: result.items)
guard let liveItem = result.items.first(where: {
contextMatchesItem(context, item: $0)
}) else {
context.notFoundAttempts += 1
return .notFound
}
guard let target = returnMoveTarget(
for: context,
items: result.items,
layoutItems: layoutItems,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: result.alwaysHiddenDividerBounds
) else {
context.rehideAttempts += 1
return .retryLater
}
do {
try await events.move(item: liveItem, to: target)
if await waitForTemporaryItemReturn(context) {
context.rehideAttempts = 0
context.notFoundAttempts = 0
return .returned
}
context.rehideAttempts += 1
MenuBarHiddenLog.plugin.warning("temporary rehide did not reach target section for \(context.identity.tag.stableKey)")
return .retryLater
} catch {
context.rehideAttempts += 1
MenuBarHiddenLog.plugin.error("temporary rehide failed: \(self.localizedDescription(for: error))")
return .retryLater
}
}
private func waitForTemporaryItemReturn(_ context: TemporarilyShownItemContext) async -> Bool {
let deadline = Date().addingTimeInterval(0.25)
while true {
if temporaryItemHasReturned(context) {
return true
}
guard Date() < deadline else {
return false
}
try? await Task.sleep(for: .milliseconds(20))
}
}
private func temporaryItemHasReturned(_ context: TemporarilyShownItemContext) -> Bool {
let result = enumerateMenuBarItems(
hiddenDividerWindowID: divider.windowID,
hiddenDividerFrame: divider.screenFrame,
alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil,
alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil,
excludedWindowIDs: excludedControlWindowIDs()
)
guard let hiddenDividerBounds = result.hiddenDividerBounds else {
return false
}
guard let liveItem = result.items.first(where: {
contextMatchesItem(context, item: $0)
}) else {
return false
}
return MenuBarHiddenLayoutPolicy.section(
for: liveItem,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: result.alwaysHiddenDividerBounds
) == context.returnDestination.section
}
private func returnMoveTarget(
for context: TemporarilyShownItemContext,
items: [MenuBarItem],
layoutItems: [MenuBarItem],
hiddenDividerBounds: CGRect,
alwaysHiddenDividerBounds: CGRect?
) -> MenuBarHiddenResolvedMoveTarget? {
let destination = context.returnDestination
if let target = computeMoveTarget(
section: destination.section,
placement: resolvedPlacement(
destination.placement,
anchor: context.primaryReturnAnchor,
items: items
),
items: items,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: alwaysHiddenDividerBounds
) {
return target
}
if let fallbackPlacement = destination.fallbackPlacement,
let target = computeMoveTarget(
section: destination.section,
placement: resolvedPlacement(
fallbackPlacement,
anchor: context.fallbackReturnAnchor,
items: items
),
items: items,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: alwaysHiddenDividerBounds
)
{
return target
}
let sectionItems = MenuBarHiddenLayoutPolicy.sectionItems(
destination.section,
from: layoutItems,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: alwaysHiddenDividerBounds
)
if let anchor = sectionItems.first {
return computeMoveTarget(
section: destination.section,
placement: .before(anchor.tag),
items: items,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: alwaysHiddenDividerBounds
)
}
return computeMoveTarget(
section: destination.section,
placement: .end,
items: items,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: alwaysHiddenDividerBounds
)
}
private func resolvedPlacement(
_ placement: MenuBarHiddenMovePlacement,
anchor: MenuBarHiddenReturnAnchor?,
items: [MenuBarItem]
) -> MenuBarHiddenMovePlacement {
guard let anchor else {
return placement
}
guard let freshItem = items.first(where: {
$0.tag.matchesIgnoringWindowID(anchor.tag)
&& ($0.sourcePID ?? $0.ownerPID) == anchor.sourcePID
}) else {
return placement
}
switch placement {
case .end:
return .end
case .before:
return .before(freshItem.tag)
case .after:
return .after(freshItem.tag)
}
}
// MARK: - Host icon recovery
private func recoverControlItemOrderIfNeeded(sessionID: Int?) {
recoverControlItemOrderIfNeeded(target: currentDragTarget, sessionID: sessionID)
}
private func recoverControlItemOrderIfNeeded(
target: MenuBarHiddenMenuBarDragTarget,
sessionID: Int?
) {
switch target {
case .hostIcon:
recoverHostIconIfNeeded(sessionID: sessionID)
case .divider:
recoverDividerIfNeeded(sessionID: sessionID)
recoverAlwaysHiddenDividerIfNeeded(sessionID: sessionID)
case .unknown:
recoverHostIconIfNeeded(sessionID: sessionID)
recoverDividerIfNeeded(sessionID: sessionID)
recoverAlwaysHiddenDividerIfNeeded(sessionID: sessionID)
}
}
private func recoverHostIconIfNeeded(sessionID: Int? = nil) {
guard divider.isInstalled else { return }
if visibleControlItemStoredPositionNeedsRecovery() || hostStatusItemNeedsRecovery() {
scheduleHostIconRecovery(sessionID: sessionID)
return
}
let result = enumerateMenuBarItems(
hiddenDividerWindowID: divider.windowID,
hiddenDividerFrame: divider.screenFrame,
alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil,
alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil,
excludedWindowIDs: excludedControlWindowIDs()
)
recoverHostIconIfNeeded(items: result.items, hiddenDividerBounds: result.hiddenDividerBounds)
}
private func recoverHostIconIfNeeded(items: [MenuBarItem], hiddenDividerBounds: CGRect?) {
guard
divider.isInstalled,
!isDraggingMenuBarItem,
!isRecoveringControlItemOrder,
MenuBarHiddenLayoutPolicy.hostIconNeedsRecovery(items: items, hiddenDividerBounds: hiddenDividerBounds)
else {
return
}
scheduleHostIconRecovery(sessionID: nil)
}
private func hostStatusItemNeedsRecovery() -> Bool {
guard
divider.isInstalled,
let dividerFrame = divider.screenFrame,
let hostFrame = hostStatusItemFrameProvider?()
else {
return false
}
let verticalOverlap = hostFrame.maxY > dividerFrame.minY && hostFrame.minY < dividerFrame.maxY
return verticalOverlap && hostFrame.maxX <= dividerFrame.minX
}
private func visibleControlItemStoredPositionNeedsRecovery() -> Bool {
MenuBarControlItemDefaults.visibleControlItemNeedsRecovery()
}
private func recoverDividerIfNeeded(sessionID: Int?) {
guard dividerNeedsRecovery() else { return }
scheduleDividerRecovery(sessionID: sessionID)
}
private func recoverAlwaysHiddenDividerIfNeeded(sessionID: Int?) {
guard
store.isAlwaysHiddenEnabled,
alwaysHiddenDivider.isInstalled,
!isDraggingMenuBarItem,
!isRecoveringControlItemOrder
else {
return
}
let result = enumerateMenuBarItems(
hiddenDividerWindowID: divider.windowID,
hiddenDividerFrame: divider.screenFrame,
alwaysHiddenDividerWindowID: alwaysHiddenDivider.windowID,
alwaysHiddenDividerFrame: alwaysHiddenDivider.screenFrame,
excludedWindowIDs: excludedControlWindowIDs()
)
guard MenuBarHiddenLayoutPolicy.alwaysHiddenDividerNeedsRecovery(
hiddenDividerBounds: result.hiddenDividerBounds,
alwaysHiddenDividerBounds: result.alwaysHiddenDividerBounds
) else {
return
}
scheduleAlwaysHiddenDividerRecovery(sessionID: sessionID)
}
private func dividerNeedsRecovery() -> Bool {
guard
divider.isInstalled,
let dividerFrame = divider.screenFrame,
let hostFrame = hostStatusItemFrameProvider?()
else {
return false
}
let verticalOverlap = hostFrame.maxY > dividerFrame.minY && hostFrame.minY < dividerFrame.maxY
return verticalOverlap && dividerFrame.minX >= hostFrame.maxX
}
private func dragTarget(at point: NSPoint?) -> MenuBarHiddenMenuBarDragTarget {
guard let point else { return .unknown }
if hostStatusItemFrameProvider?().map({ $0.insetBy(dx: -6, dy: -6).contains(point) }) == true {
return .hostIcon
}
if divider.screenFrame.map({ $0.insetBy(dx: -6, dy: -6).contains(point) }) == true {
return .divider
}
return .unknown
}
private func startControlItemOrderRecoveryPolling(sessionID: Int) {
controlItemRecoveryPollTask?.cancel()
controlItemRecoveryPollTask = Task { [weak self] in
while !Task.isCancelled {
try? await Task.sleep(for: .milliseconds(120))
guard !Task.isCancelled else { return }
await MainActor.run {
guard !Task.isCancelled else { return }
guard let self else { return }
guard self.dragSessionID == sessionID else {
self.stopControlItemOrderRecoveryPolling()
return
}
guard self.isDraggingMenuBarItem else {
self.stopControlItemOrderRecoveryPolling()
return
}
guard !Self.isLeftMouseButtonPressed else { return }
self.isDraggingMenuBarItem = false
self.stopControlItemOrderRecoveryPolling()
self.scheduleMenuBarDragCommit(
target: self.currentDragTarget,
sessionID: sessionID
)
self.currentDragTarget = .unknown
}
}
}
}
private func stopControlItemOrderRecoveryPolling() {
controlItemRecoveryPollTask?.cancel()
controlItemRecoveryPollTask = nil
}
private static var isLeftMouseButtonPressed: Bool {
(NSEvent.pressedMouseButtons & 1) != 0
}
private func waitForMenuBarFramesToSettle() async {
var previousSignature = menuBarFrameSignature()
var stableReads = 0
let deadline = Date().addingTimeInterval(0.45)
while Date() < deadline {
try? await Task.sleep(for: .milliseconds(45))
guard !Task.isCancelled else { return }
let currentSignature = menuBarFrameSignature()
if currentSignature == previousSignature {
stableReads += 1
if stableReads >= 2 {
return
}
} else {
stableReads = 0
previousSignature = currentSignature
}
}
}
private func menuBarFrameSignature() -> [String] {
let result = enumerateMenuBarItems(
hiddenDividerWindowID: divider.windowID,
hiddenDividerFrame: divider.screenFrame,
alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil,
alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil,
excludedWindowIDs: excludedControlWindowIDs()
)
return result.items.map { item in
let bounds = item.bounds.integral
return "\(item.tag.stableKey):\(Int(bounds.minX)):\(Int(bounds.maxX))"
}
}
private var isMenuBarDragCommitPending: Bool {
menuBarDragCommitSessionID != nil
}
private func scheduleMenuBarDragCommit(target: MenuBarHiddenMenuBarDragTarget, sessionID: Int) {
cancelMenuBarDragCommit()
menuBarDragCommitSessionID = sessionID
menuBarDragCommitTask = Task { [weak self] in
try? await Task.sleep(for: .milliseconds(120))
guard !Task.isCancelled else { return }
guard let self else { return }
await self.waitForMenuBarFramesToSettle()
guard !Task.isCancelled else { return }
await MainActor.run {
guard !Task.isCancelled else { return }
guard
self.dragSessionID == sessionID,
!self.isDraggingMenuBarItem
else {
self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID)
return
}
self.commitMenuBarDragLayout(target: target, sessionID: sessionID)
}
}
}
private func commitMenuBarDragLayout(
target: MenuBarHiddenMenuBarDragTarget,
sessionID: Int
) {
let decision = MenuBarHiddenMenuBarDragCommitPolicy.decision(
target: target,
dividerNeedsRecovery: dividerNeedsRecovery()
)
switch decision {
case .commit:
recordStoredLayoutAfterMenuBarDrag()
finishMenuBarDragCommit(sessionID: sessionID)
case .recoverThenCommit:
recoverDividerThenCommitMenuBarDrag(sessionID: sessionID)
case .recoverOnly:
recoverControlItemOrderIfNeeded(target: target, sessionID: sessionID)
if !isRecoveringControlItemOrder {
finishMenuBarDragCommit(sessionID: sessionID)
}
}
}
private func cancelMenuBarDragCommit() {
menuBarDragCommitTask?.cancel()
menuBarDragCommitTask = nil
menuBarDragCommitSessionID = nil
}
private func isMenuBarDragCommitCurrent(sessionID: Int) -> Bool {
menuBarDragCommitSessionID == sessionID
}
private func clearMenuBarDragCommitIfCurrent(sessionID: Int) {
guard menuBarDragCommitSessionID == sessionID else { return }
menuBarDragCommitTask = nil
menuBarDragCommitSessionID = nil
}
private func scheduleHostIconRecovery(sessionID: Int?) {
guard !isRecoveringControlItemOrder else { return }
isRecoveringControlItemOrder = true
controlItemRecoveryTask?.cancel()
controlItemRecoveryTask = Task { [weak self] in
guard !Task.isCancelled else { return }
await MainActor.run {
guard !Task.isCancelled else { return }
guard let self else { return }
if let sessionID {
guard self.dragSessionID == sessionID else {
self.isRecoveringControlItemOrder = false
self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID)
return
}
}
self.divider.showSection(isDragging: false)
self.resetHostStatusItemPosition?()
}
try? await Task.sleep(for: .milliseconds(400))
guard !Task.isCancelled else { return }
await MainActor.run {
guard !Task.isCancelled else { return }
guard let self else { return }
if let sessionID {
guard self.dragSessionID == sessionID else {
self.isRecoveringControlItemOrder = false
self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID)
return
}
}
self.isRecoveringControlItemOrder = false
if let sessionID, self.isMenuBarDragCommitCurrent(sessionID: sessionID) {
self.finishMenuBarDragCommit(sessionID: sessionID)
return
}
self.refresh(reason: .hostIconRecovered)
if self.shouldRestoreHiddenAfterDrag {
self.scheduleHiddenRestoreAfterDrag(delay: .milliseconds(250), sessionID: sessionID)
} else if self.store.isEnabled, self.divider.isInstalled, !self.isDraggingMenuBarItem {
self.installAndExpand()
}
self.updateAlwaysHiddenDividerVisibility()
}
}
}
private func scheduleDividerRecovery(sessionID: Int?) {
guard !isRecoveringControlItemOrder else { return }
isRecoveringControlItemOrder = true
controlItemRecoveryTask?.cancel()
controlItemRecoveryTask = Task { [weak self] in
guard !Task.isCancelled else { return }
await MainActor.run {
guard !Task.isCancelled else { return }
guard let self else { return }
if let sessionID {
guard self.dragSessionID == sessionID else {
self.isRecoveringControlItemOrder = false
self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID)
return
}
}
self.divider.showSection(isDragging: false)
self.divider.reinstall()
}
try? await Task.sleep(for: .milliseconds(400))
guard !Task.isCancelled else { return }
await MainActor.run {
guard !Task.isCancelled else { return }
guard let self else { return }
if let sessionID {
guard self.dragSessionID == sessionID else {
self.isRecoveringControlItemOrder = false
self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID)
return
}
}
self.isRecoveringControlItemOrder = false
self.refresh(reason: .hostIconRecovered)
if self.shouldRestoreHiddenAfterDrag {
self.scheduleHiddenRestoreAfterDrag(delay: .milliseconds(250), sessionID: sessionID)
} else if self.store.isEnabled, self.divider.isInstalled, !self.isDraggingMenuBarItem {
self.divider.hideSection()
}
self.updateAlwaysHiddenDividerVisibility()
}
}
}
private func recoverDividerThenCommitMenuBarDrag(sessionID: Int) {
guard !isRecoveringControlItemOrder else { return }
isRecoveringControlItemOrder = true
controlItemRecoveryTask?.cancel()
controlItemRecoveryTask = Task { [weak self] in
guard !Task.isCancelled else { return }
await MainActor.run {
guard !Task.isCancelled else { return }
guard let self else { return }
guard self.dragSessionID == sessionID else {
self.isRecoveringControlItemOrder = false
self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID)
return
}
self.divider.showSection(isDragging: false)
self.divider.reinstall()
}
try? await Task.sleep(for: .milliseconds(250))
guard !Task.isCancelled else { return }
guard let self else { return }
await self.waitForMenuBarFramesToSettle()
guard !Task.isCancelled else { return }
await MainActor.run {
guard !Task.isCancelled else { return }
guard self.dragSessionID == sessionID else {
self.isRecoveringControlItemOrder = false
self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID)
return
}
self.isRecoveringControlItemOrder = false
self.recordStoredLayoutAfterMenuBarDrag()
self.finishMenuBarDragCommit(sessionID: sessionID)
}
}
}
private func scheduleAlwaysHiddenDividerRecovery(sessionID: Int?) {
guard !isRecoveringControlItemOrder else { return }
isRecoveringControlItemOrder = true
controlItemRecoveryTask?.cancel()
controlItemRecoveryTask = Task { [weak self] in
guard !Task.isCancelled else { return }
await MainActor.run {
guard !Task.isCancelled else { return }
guard let self else { return }
if let sessionID {
guard self.dragSessionID == sessionID else {
self.isRecoveringControlItemOrder = false
self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID)
return
}
}
self.alwaysHiddenDivider.showSection(isDragging: false)
self.divider.showSection(isDragging: false)
}
try? await Task.sleep(for: .milliseconds(150))
guard !Task.isCancelled else { return }
await MainActor.run {
guard !Task.isCancelled else { return }
guard let self else { return }
if let sessionID {
guard self.dragSessionID == sessionID else {
self.isRecoveringControlItemOrder = false
self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID)
return
}
}
let result = self.enumerateMenuBarItems(
hiddenDividerWindowID: self.divider.windowID,
hiddenDividerFrame: self.divider.screenFrame,
alwaysHiddenDividerWindowID: self.alwaysHiddenDivider.windowID,
alwaysHiddenDividerFrame: self.alwaysHiddenDivider.screenFrame,
excludedWindowIDs: self.excludedControlWindowIDs()
)
guard
let alwaysHiddenDividerWindowID = self.alwaysHiddenDivider.windowID,
let hiddenDividerBounds = result.hiddenDividerBounds,
MenuBarHiddenLayoutPolicy.alwaysHiddenDividerNeedsRecovery(
hiddenDividerBounds: result.hiddenDividerBounds,
alwaysHiddenDividerBounds: result.alwaysHiddenDividerBounds
)
else {
self.isRecoveringControlItemOrder = false
self.restoreControlItemsAfterAlwaysHiddenRecovery()
self.rebuildSnapshot()
return
}
let controlItem = MenuBarItem(
tag: MenuBarItemTag(
namespace: "\(ProcessInfo.processInfo.processIdentifier)",
title: MenuBarHiddenConstants.alwaysHiddenControlItemTitle,
windowID: alwaysHiddenDividerWindowID,
instanceIndex: 0
),
windowID: alwaysHiddenDividerWindowID,
ownerPID: ProcessInfo.processInfo.processIdentifier,
bounds: result.alwaysHiddenDividerBounds ?? .zero,
title: MenuBarHiddenConstants.alwaysHiddenControlItemTitle,
isOnScreen: true
)
let target = MenuBarHiddenResolvedMoveTarget(
point: CGPoint(x: hiddenDividerBounds.minX, y: hiddenDividerBounds.minY),
windowID: self.divider.windowID
)
self.controlItemRecoveryTask = Task { [weak self] in
guard let self else { return }
do {
try await self.events.move(item: controlItem, to: target)
} catch {
MenuBarHiddenLog.plugin.error(
"always-hidden divider recovery failed: \(self.localizedDescription(for: error))"
)
}
try? await Task.sleep(for: .milliseconds(300))
guard !Task.isCancelled else { return }
await MainActor.run {
guard !Task.isCancelled else { return }
if let sessionID {
guard self.dragSessionID == sessionID else {
self.isRecoveringControlItemOrder = false
self.clearMenuBarDragCommitIfCurrent(sessionID: sessionID)
return
}
}
self.isRecoveringControlItemOrder = false
self.restoreControlItemsAfterAlwaysHiddenRecovery()
self.rebuildSnapshot()
self.refreshIconsIfUIVisible()
}
}
}
}
}
private func recordStoredLayoutAfterMenuBarDrag() {
guard permissions.canManageItems else { return }
let result = enumerateMenuBarItems(
hiddenDividerWindowID: divider.windowID,
hiddenDividerFrame: divider.screenFrame,
alwaysHiddenDividerWindowID: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.windowID : nil,
alwaysHiddenDividerFrame: alwaysHiddenDivider.isInstalled ? alwaysHiddenDivider.screenFrame : nil,
excludedWindowIDs: excludedControlWindowIDs()
)
guard let hiddenDividerBounds = result.hiddenDividerBounds else {
return
}
let layoutItems = MenuBarHiddenLayoutPolicy.layoutEditorItems(from: result.items)
let alwaysHiddenDividerBounds: CGRect?
if store.isAlwaysHiddenEnabled {
guard let bounds = result.alwaysHiddenDividerBounds else { return }
alwaysHiddenDividerBounds = bounds
} else {
alwaysHiddenDividerBounds = nil
}
let visibleItems = MenuBarHiddenLayoutPolicy.visibleItems(
from: layoutItems,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: alwaysHiddenDividerBounds
)
let hiddenItems = MenuBarHiddenLayoutPolicy.hiddenItems(
from: layoutItems,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: alwaysHiddenDividerBounds
)
let alwaysHiddenItems = MenuBarHiddenLayoutPolicy.alwaysHiddenItems(
from: layoutItems,
hiddenDividerBounds: hiddenDividerBounds,
alwaysHiddenDividerBounds: alwaysHiddenDividerBounds
)
let nextVisibleHidden = MenuBarHiddenStoredLayoutPolicy.visibleHiddenLayout(
visibleItems: visibleItems,
hiddenItems: hiddenItems,
alwaysHiddenItems: alwaysHiddenItems,
previousVisibleItemStableKeys: store.visibleItemStableKeys,
previousHiddenItemStableKeys: store.hiddenItemStableKeys
)
store.recordVisibleHiddenLayout(
visibleKeys: nextVisibleHidden.visibleItemStableKeys,
hiddenKeys: nextVisibleHidden.hiddenItemStableKeys
)
guard store.isAlwaysHiddenEnabled else { return }
let currentAlwaysHiddenKeys = Set(alwaysHiddenItems.filter(\.canBeHidden).map(\.tag.stableKey))
let currentItemKeys = Set(layoutItems.map(\.tag.stableKey))
let previousKeys = store.alwaysHiddenItemStableKeys
let keysStillMissing = previousKeys.subtracting(currentItemKeys)
let nextKeys = keysStillMissing.union(currentAlwaysHiddenKeys)
guard nextKeys != previousKeys else { return }
store.alwaysHiddenItemStableKeys = nextKeys
}
private func finishMenuBarDragCommit(sessionID: Int) {
guard dragSessionID == sessionID else {
clearMenuBarDragCommitIfCurrent(sessionID: sessionID)
return
}
clearMenuBarDragCommitIfCurrent(sessionID: sessionID)
rebuildSnapshot()
refreshIconsIfUIVisible()
if shouldRestoreHiddenAfterDrag {
scheduleHiddenRestoreAfterDrag(delay: .milliseconds(80), sessionID: sessionID)
}
scheduleAlwaysHiddenItemsRestoreIfNeeded(delay: .milliseconds(400))
}
private func restoreControlItemsAfterAlwaysHiddenRecovery() {
if store.isEnabled, divider.isInstalled, !isDraggingMenuBarItem {
divider.hideSection()
} else if divider.isInstalled {
divider.showSection(isDragging: isDraggingMenuBarItem)
}
if store.isAlwaysHiddenEnabled {
if alwaysHiddenDivider.isInstalled {
alwaysHiddenDivider.hideSection()
}
} else {
alwaysHiddenDivider.uninstall()
}
}
private func scheduleHiddenRestoreAfterDrag(delay: Duration, sessionID: Int?) {
guard shouldRestoreHiddenAfterDrag else { return }
hiddenRestoreTask?.cancel()
hiddenRestoreTask = Task { [weak self] in
try? await Task.sleep(for: delay)
guard !Task.isCancelled else { return }
await MainActor.run {
guard !Task.isCancelled else { return }
guard let self else { return }
if let sessionID {
guard self.dragSessionID == sessionID else { return }
}
guard self.shouldRestoreHiddenAfterDrag else { return }
guard !self.isDraggingMenuBarItem, !self.isRecoveringControlItemOrder else {
self.scheduleHiddenRestoreAfterDrag(delay: .milliseconds(250), sessionID: sessionID)
return
}
self.shouldRestoreHiddenAfterDrag = false
self.store.isEnabled = true
self.installAndExpand()
self.rebuildSnapshot()
self.scheduleAlwaysHiddenItemsRestoreIfNeeded()
}
}
}
}