import AppKit
|
import QuartzCore
|
import AlignerCore
|
|
@MainActor
|
final class QuickSwitchSessionController {
|
static let overlayTitle = "Aligner Quick Switch"
|
private static let closeRequestStillPresentMessage = "已发送关闭请求,但窗口仍在列表中。"
|
private static let closeTargetNotFoundMessage = "没有找到这个窗口的关闭控件,已刷新列表。"
|
private static let closeUnsupportedMessage = "这个窗口没有可用的关闭按钮。"
|
private static let closeFailedMessage = "关闭请求没有被系统接受。"
|
private static let closeVerificationRetryDelays: [TimeInterval] = [0.35, 0.70, 1.20]
|
|
private let view = QuickSwitchRootView()
|
private let snapshotLoader: any QuickSwitchSnapshotLoading
|
private let screenshotProvider: any ScreenshotProviderProtocol
|
private let windowActivationService: any WindowActivationServiceProtocol
|
private let spaceActivationService: any SpaceActivationServiceProtocol
|
private let windowCloseService: any WindowCloseServiceProtocol
|
private let systemCriticalWindowDetector: any SystemCriticalWindowDetecting
|
private let disableScreenshotRefresh: Bool
|
private var waterfallViewMode: QuickSwitchWaterfallViewMode
|
private var theme: ThemePreference
|
private var closeConfirmationRequired: Bool
|
private let onCloseConfirmationDisabled: (() -> Void)?
|
private let debugOverlayWidth: CGFloat?
|
private let debugKeySequence: [String]
|
private let debugMouseSequence: [String]
|
private let debugSystemCriticalAfter: TimeInterval?
|
private lazy var coordinator = WindowCoordinator(
|
contentView: view,
|
title: Self.overlayTitle,
|
debugOverlayWidth: debugOverlayWidth
|
)
|
private var currentSnapshot: QuickSwitchSnapshot?
|
private var sourceViewModel: QuickSwitchViewModel?
|
private var currentViewModel: QuickSwitchViewModel?
|
private var lockedSpaceFilterID: UInt64?
|
private var selectionBeforeSpaceFilter: QuickSwitchSelection?
|
private var lastSpaceFilterAction: String?
|
private var pendingLockedSpaceUnlockSpaceID: UInt64?
|
private var pendingLockedSpaceUnlockTask: Task<Void, Never>?
|
private var lastSnapshotError: String?
|
private var snapshotTask: Task<Void, Never>?
|
private var screenshotTask: Task<Void, Never>?
|
private var systemCriticalMonitorTimer: Timer?
|
private var debugSystemCriticalTimer: Timer?
|
private var screenshotSession = ScreenshotCaptureSession()
|
private var sessionGeneration = 0
|
private var showStartTime: CFTimeInterval?
|
private var overlayOpenElapsedMilliseconds: Double?
|
private var snapshotStartElapsedMilliseconds: Double?
|
private var snapshotDurationMilliseconds: Double?
|
private var snapshotRanOnMainThread: Bool?
|
private var lastCommittedSelection: QuickSwitchSelection?
|
private var lastCommitSource: QuickSwitchCommitSource?
|
private var lastActivationWindowID: UInt32?
|
private var lastActivationResult: WindowActivationResult?
|
private var lastActivationError: String?
|
private var lastSpaceActivationSpaceID: UInt64?
|
private var lastSpaceActivationDidRequestFocus: Bool?
|
private var lastSpaceActivationDisplayIdentifier: String?
|
private var lastSpaceActivationPreviousCurrentSpaceID: UInt64?
|
private var lastSpaceActivationError: String?
|
private var lastCloseTargetKind: String?
|
private var lastCloseAppGroupIndex: Int?
|
private var lastCloseAppName: String?
|
private var lastCloseWindowID: UInt32?
|
private var lastCloseResult: WindowCloseResult?
|
private var lastCloseError: String?
|
private var pendingCloseVerification: PendingCloseVerification?
|
private var suppressedCloseTargets: [SuppressedCloseTarget] = []
|
private var lastDismissReason: DismissReason?
|
private var lastSystemCriticalAction: String?
|
private var lastSystemCriticalMonitorSource: String?
|
private var lastSystemCriticalWindowCount = 0
|
private var lastSystemCriticalWindowTitleHashes: [String] = []
|
private var lastSystemCriticalWindowTitleLengths: [Int] = []
|
private var lastSystemCriticalWindowTitleIsEmpty: [Bool] = []
|
private var lastSystemCriticalWindowOwners: [String] = []
|
private var lastSystemCriticalOverlayLevel: CGWindowLevel?
|
private var systemCriticalDetectionCount = 0
|
private var lifecycleTargetCycles = 0
|
private var lifecycleCompletedCycles = 0
|
private var lifecycleMaximumOverlayWindows = 0
|
private var lifecycleMaximumVisibleOverlayWindows = 0
|
private var lifecycleResidualOverlayWindows = 0
|
private var lifecycleResidualVisibleOverlayWindows = 0
|
var onSnapshotUpdated: (() -> Void)?
|
var onToggleWaterfallViewMode: (() -> Void)?
|
var onToggleTheme: (() -> Void)?
|
|
private struct PendingCloseVerification {
|
let request: QuickSwitchCloseRequest
|
let generation: Int
|
let attempt: Int
|
}
|
|
private enum SuppressedCloseTarget: Equatable {
|
case app(AlignerApp)
|
case window(UInt32)
|
|
init(_ request: QuickSwitchCloseRequest) {
|
switch request {
|
case .app:
|
self = .app(request.app)
|
case .window:
|
self = .window(request.windowID ?? 0)
|
}
|
}
|
}
|
|
init(
|
snapshotLoader: any QuickSwitchSnapshotLoading = LiveQuickSwitchSnapshotLoader(),
|
screenshotProvider: any ScreenshotProviderProtocol = ScreenCaptureKitScreenshotProvider(
|
debugLogger: StderrScreenshotDebugLogger()
|
),
|
windowActivationService: any WindowActivationServiceProtocol = CGWindowAXWindowService(
|
spaceIDsByWindowIDProvider: { _ in [:] }
|
),
|
spaceActivationService: any SpaceActivationServiceProtocol = PrivateSpaceActivationService(),
|
windowCloseService: any WindowCloseServiceProtocol = CGWindowAXWindowService(
|
spaceIDsByWindowIDProvider: { _ in [:] }
|
),
|
systemCriticalWindowDetector: any SystemCriticalWindowDetecting = CGWindowSystemCriticalWindowDetector(),
|
disableScreenshotRefresh: Bool = false,
|
waterfallViewMode: QuickSwitchWaterfallViewMode = .verticalColumns,
|
theme: ThemePreference = .light,
|
closeConfirmationRequired: Bool = true,
|
onCloseConfirmationDisabled: (() -> Void)? = nil,
|
debugHoveredAppGroupIndex: Int? = nil,
|
debugOverlayWidth: CGFloat? = nil,
|
debugKeySequence: [String] = [],
|
debugMouseSequence: [String] = [],
|
debugSystemCriticalAfter: TimeInterval? = nil
|
) {
|
self.snapshotLoader = snapshotLoader
|
self.screenshotProvider = screenshotProvider
|
self.windowActivationService = windowActivationService
|
self.spaceActivationService = spaceActivationService
|
self.windowCloseService = windowCloseService
|
self.systemCriticalWindowDetector = systemCriticalWindowDetector
|
self.disableScreenshotRefresh = disableScreenshotRefresh
|
self.waterfallViewMode = waterfallViewMode
|
self.theme = theme
|
self.closeConfirmationRequired = closeConfirmationRequired
|
self.onCloseConfirmationDisabled = onCloseConfirmationDisabled
|
self.debugOverlayWidth = debugOverlayWidth
|
self.debugKeySequence = debugKeySequence
|
self.debugMouseSequence = debugMouseSequence
|
self.debugSystemCriticalAfter = debugSystemCriticalAfter
|
view.setDebugHoveredAppGroupIndex(debugHoveredAppGroupIndex)
|
}
|
|
var isVisible: Bool {
|
coordinator.isQuickSwitchVisible
|
}
|
|
func show() {
|
sessionGeneration += 1
|
let generation = sessionGeneration
|
DevelopmentDiagnostics.log("quickSwitch.session.show.start", [
|
"generation": generation,
|
"wasVisible": isVisible
|
])
|
showStartTime = CACurrentMediaTime()
|
overlayOpenElapsedMilliseconds = nil
|
snapshotStartElapsedMilliseconds = nil
|
snapshotDurationMilliseconds = nil
|
snapshotRanOnMainThread = nil
|
lastCommittedSelection = nil
|
lastCommitSource = nil
|
lastActivationWindowID = nil
|
lastActivationResult = nil
|
lastActivationError = nil
|
lastSpaceActivationSpaceID = nil
|
lastSpaceActivationDidRequestFocus = nil
|
lastSpaceActivationDisplayIdentifier = nil
|
lastSpaceActivationPreviousCurrentSpaceID = nil
|
lastSpaceActivationError = nil
|
lastCloseTargetKind = nil
|
lastCloseAppGroupIndex = nil
|
lastCloseAppName = nil
|
lastCloseWindowID = nil
|
lastCloseResult = nil
|
lastCloseError = nil
|
pendingCloseVerification = nil
|
suppressedCloseTargets = []
|
cancelPendingLockedSpaceUnlock()
|
lockedSpaceFilterID = nil
|
selectionBeforeSpaceFilter = nil
|
lastSpaceFilterAction = nil
|
lastDismissReason = nil
|
lastSystemCriticalAction = nil
|
lastSystemCriticalMonitorSource = nil
|
lastSystemCriticalWindowCount = 0
|
lastSystemCriticalWindowTitleHashes = []
|
lastSystemCriticalWindowTitleLengths = []
|
lastSystemCriticalWindowTitleIsEmpty = []
|
lastSystemCriticalWindowOwners = []
|
lastSystemCriticalOverlayLevel = nil
|
systemCriticalDetectionCount = 0
|
invalidateSystemCriticalMonitoring()
|
screenshotTask?.cancel()
|
screenshotTask = nil
|
screenshotSession = ScreenshotCaptureSession()
|
clearSnapshotState()
|
view.onEscape = { [weak self] in
|
self?.hide(reason: .escape)
|
}
|
view.onCommitSelection = { [weak self] selection, source in
|
self?.commitSelection(selection, source: source)
|
}
|
view.onSpaceLaneClick = { [weak self] spaceID, clickCount in
|
self?.handleSpaceLaneClick(spaceID, clickCount: clickCount)
|
}
|
view.onBackgroundClick = { [weak self] in
|
self?.handleBackgroundClick() ?? false
|
}
|
view.onToggleWaterfallViewMode = { [weak self] in
|
self?.onToggleWaterfallViewMode?()
|
}
|
view.onToggleTheme = { [weak self] in
|
self?.onToggleTheme?()
|
}
|
view.setSuppressEventInput(!debugMouseSequence.isEmpty || !debugKeySequence.isEmpty)
|
view.onRequestClose = { [weak self] request in
|
self?.requestClose(request)
|
}
|
view.onCloseConfirmationDisabled = { [weak self] in
|
guard let self else { return }
|
self.closeConfirmationRequired = false
|
self.onCloseConfirmationDisabled?()
|
}
|
view.setCloseConfirmationRequired(closeConfirmationRequired)
|
view.setWaterfallViewMode(waterfallViewMode)
|
view.setTheme(theme)
|
view.apply(viewModel: nil)
|
view.beginPerformanceFirstFrameMeasurement()
|
coordinator.openQuickSwitch()
|
coordinator.promoteForTextInput()
|
view.window?.makeFirstResponder(view)
|
overlayOpenElapsedMilliseconds = elapsedSinceShowStart()
|
updateLifecycleWindowHighWaterMark()
|
startSystemCriticalMonitoring()
|
scheduleSnapshotRefresh(for: generation)
|
DevelopmentDiagnostics.log("quickSwitch.session.show.end", [
|
"generation": generation,
|
"overlayOpenElapsedMilliseconds": overlayOpenElapsedMilliseconds,
|
"overlayLevel": coordinator.currentLevel,
|
"windowVisible": view.window?.isVisible ?? false
|
])
|
}
|
|
func hide(reason: DismissReason = .userClosed) {
|
DevelopmentDiagnostics.log("quickSwitch.session.hide.start", [
|
"reason": Self.dismissReasonString(reason),
|
"wasVisible": isVisible,
|
"generation": sessionGeneration
|
])
|
sessionGeneration += 1
|
lastDismissReason = reason
|
snapshotTask?.cancel()
|
snapshotTask = nil
|
screenshotTask?.cancel()
|
screenshotTask = nil
|
cancelPendingLockedSpaceUnlock()
|
pendingCloseVerification = nil
|
view.clearCloseFeedback()
|
invalidateSystemCriticalMonitoring()
|
coordinator.closeQuickSwitch(reason: reason)
|
lifecycleResidualOverlayWindows = overlayWindowCount()
|
lifecycleResidualVisibleOverlayWindows = visibleOverlayWindowCount()
|
updateLifecycleWindowHighWaterMark()
|
DevelopmentDiagnostics.log("quickSwitch.session.hide.end", [
|
"reason": Self.dismissReasonString(reason),
|
"generation": sessionGeneration,
|
"residualOverlayWindows": lifecycleResidualOverlayWindows,
|
"residualVisibleOverlayWindows": lifecycleResidualVisibleOverlayWindows
|
])
|
}
|
|
func refocus() {
|
DevelopmentDiagnostics.log("quickSwitch.session.refocus")
|
coordinator.promoteForTextInput()
|
view.window?.makeFirstResponder(view)
|
}
|
|
func retriggerFromShortcut() {
|
DevelopmentDiagnostics.log("quickSwitch.session.retriggerFromShortcut")
|
guard isVisible else {
|
show()
|
return
|
}
|
|
guard currentViewModel != nil else {
|
DevelopmentDiagnostics.log("quickSwitch.session.retriggerFromShortcut.noSnapshot", [
|
"action": "fallbackShow"
|
])
|
show()
|
return
|
}
|
|
let didCycle = view.hoverNextAppGroupIndex()
|
DevelopmentDiagnostics.log("quickSwitch.session.retriggerFromShortcut.hoverApp", [
|
"didCycle": didCycle
|
])
|
|
if !didCycle {
|
show()
|
}
|
}
|
|
func setWaterfallViewMode(_ mode: QuickSwitchWaterfallViewMode) {
|
guard waterfallViewMode != mode else { return }
|
|
waterfallViewMode = mode
|
DevelopmentDiagnostics.log("quickSwitch.session.waterfallViewModeChanged", [
|
"mode": mode.rawValue,
|
"visible": isVisible,
|
"hasViewModel": currentViewModel != nil
|
])
|
|
view.setWaterfallViewMode(mode)
|
if isVisible, let currentViewModel {
|
view.applyProjected(viewModel: currentViewModel)
|
}
|
onSnapshotUpdated?()
|
}
|
|
func setTheme(_ theme: ThemePreference) {
|
guard self.theme != theme else { return }
|
|
self.theme = theme
|
DevelopmentDiagnostics.log("quickSwitch.session.themeChanged", [
|
"theme": theme.rawValue,
|
"visible": isVisible,
|
"hasViewModel": currentViewModel != nil
|
])
|
|
view.setTheme(theme)
|
onSnapshotUpdated?()
|
}
|
|
func reportDictionary() -> [String: Any] {
|
let spaceCount = currentViewModel?.spaceCount ?? 0
|
let appCount = currentViewModel?.appShelf.count ?? 0
|
let columnCount = currentViewModel?.waterfallColumns.count ?? 0
|
let windowCount = currentViewModel?.windowCount ?? 0
|
|
return [
|
"snapshotLoaded": currentSnapshot != nil,
|
"snapshotError": lastSnapshotError ?? NSNull(),
|
"quickSwitchVisible": isVisible,
|
"diagnosticLogLocationKind": DevelopmentDiagnostics.pathSummary(DevelopmentDiagnostics.logPath)["locationKind"] ?? "unknown",
|
"diagnosticLogBasename": DevelopmentDiagnostics.pathSummary(DevelopmentDiagnostics.logPath)["basename"] ?? "aligner-dev.log",
|
"displayCount": currentViewModel?.displays.count ?? 0,
|
"spaceCount": spaceCount,
|
"appCount": appCount,
|
"columnCount": columnCount,
|
"windowCount": windowCount,
|
"initialSelectionWindowID": currentViewModel?.initialSelection?.windowID ?? NSNull(),
|
"appShelfNames": currentViewModel?.appShelf.map(\.app.name) ?? [],
|
"spaceLabels": currentViewModel?.displays.flatMap { $0.spaces.map(\.label) } ?? [],
|
"spaceFilterLockedSpaceID": lockedSpaceFilterID ?? NSNull(),
|
"spaceFilterActive": lockedSpaceFilterID != nil,
|
"lastSpaceFilterAction": lastSpaceFilterAction ?? NSNull(),
|
"overlayOpenElapsedMilliseconds": overlayOpenElapsedMilliseconds ?? NSNull(),
|
"snapshotStartElapsedMilliseconds": snapshotStartElapsedMilliseconds ?? NSNull(),
|
"snapshotDurationMilliseconds": snapshotDurationMilliseconds ?? NSNull(),
|
"snapshotRanOnMainThread": snapshotRanOnMainThread ?? NSNull(),
|
"lastCommittedAppGroupIndex": lastCommittedSelection?.appGroupIndex ?? NSNull(),
|
"lastCommittedWindowIndex": lastCommittedSelection?.windowIndex ?? NSNull(),
|
"lastCommittedWindowID": lastCommittedSelection?.windowID ?? NSNull(),
|
"lastCommitSource": lastCommitSource?.rawValue ?? NSNull(),
|
"lastActivationWindowID": lastActivationWindowID ?? NSNull(),
|
"lastActivationResult": lastActivationResult.map(Self.activationResultString) ?? NSNull(),
|
"lastActivationError": lastActivationError ?? NSNull(),
|
"lastSpaceActivationSpaceID": lastSpaceActivationSpaceID ?? NSNull(),
|
"lastSpaceActivationDidRequestFocus": lastSpaceActivationDidRequestFocus ?? NSNull(),
|
"lastSpaceActivationDisplayIdentifier": lastSpaceActivationDisplayIdentifier ?? NSNull(),
|
"lastSpaceActivationPreviousCurrentSpaceID": lastSpaceActivationPreviousCurrentSpaceID ?? NSNull(),
|
"lastSpaceActivationError": lastSpaceActivationError ?? NSNull(),
|
"lastCloseTargetKind": lastCloseTargetKind ?? NSNull(),
|
"lastCloseAppGroupIndex": lastCloseAppGroupIndex ?? NSNull(),
|
"lastCloseAppName": lastCloseAppName ?? NSNull(),
|
"lastCloseWindowID": lastCloseWindowID ?? NSNull(),
|
"lastCloseResult": lastCloseResult.map(Self.closeResultString) ?? NSNull(),
|
"lastCloseError": lastCloseError ?? NSNull(),
|
"suppressedCloseTargetCount": suppressedCloseTargets.count,
|
"lastDismissReason": lastDismissReason.map(Self.dismissReasonString) ?? NSNull(),
|
"overlayLevel": Int(coordinator.currentLevel),
|
"overlayMainMenuLevel": Int(CGWindowLevelForKey(.mainMenuWindow)),
|
"overlayBelowRescueSystemWindows": coordinator.currentLevel < CGWindowLevelForKey(.mainMenuWindow),
|
"lastSystemCriticalAction": lastSystemCriticalAction ?? NSNull(),
|
"lastSystemCriticalMonitorSource": lastSystemCriticalMonitorSource ?? NSNull(),
|
"lastSystemCriticalWindowCount": lastSystemCriticalWindowCount,
|
"lastSystemCriticalWindowTitleHashes": lastSystemCriticalWindowTitleHashes,
|
"lastSystemCriticalWindowTitleLengths": lastSystemCriticalWindowTitleLengths,
|
"lastSystemCriticalWindowTitleIsEmpty": lastSystemCriticalWindowTitleIsEmpty,
|
"lastSystemCriticalWindowOwners": lastSystemCriticalWindowOwners,
|
"lastSystemCriticalOverlayLevel": lastSystemCriticalOverlayLevel.map { Int($0) } ?? NSNull(),
|
"systemCriticalDetectionCount": systemCriticalDetectionCount,
|
"lifecycleTargetCycles": lifecycleTargetCycles,
|
"lifecycleCompletedCycles": lifecycleCompletedCycles,
|
"lifecycleMaximumOverlayWindows": lifecycleMaximumOverlayWindows,
|
"lifecycleMaximumVisibleOverlayWindows": lifecycleMaximumVisibleOverlayWindows,
|
"lifecycleResidualOverlayWindows": lifecycleResidualOverlayWindows,
|
"lifecycleResidualVisibleOverlayWindows": lifecycleResidualVisibleOverlayWindows,
|
"lifecycleGatePassed": lifecycleTargetCycles == 0
|
|| (
|
lifecycleCompletedCycles == lifecycleTargetCycles
|
&& lifecycleMaximumOverlayWindows <= 1
|
&& lifecycleMaximumVisibleOverlayWindows <= 1
|
&& lifecycleResidualOverlayWindows <= 1
|
&& lifecycleResidualVisibleOverlayWindows == 0
|
),
|
"rootView": view.reportDictionary()
|
]
|
}
|
|
func prepareLifecycleRun(targetCycles: Int) {
|
lifecycleTargetCycles = max(0, targetCycles)
|
lifecycleCompletedCycles = 0
|
lifecycleMaximumOverlayWindows = 0
|
lifecycleMaximumVisibleOverlayWindows = 0
|
lifecycleResidualOverlayWindows = 0
|
lifecycleResidualVisibleOverlayWindows = 0
|
view.resetPerformanceMeasurements()
|
}
|
|
func markLifecycleCycleCompleted() {
|
lifecycleCompletedCycles += 1
|
updateLifecycleWindowHighWaterMark()
|
}
|
|
func finalizeLifecycleRun() {
|
lifecycleResidualOverlayWindows = overlayWindowCount()
|
lifecycleResidualVisibleOverlayWindows = visibleOverlayWindowCount()
|
updateLifecycleWindowHighWaterMark()
|
}
|
|
private func scheduleSnapshotRefresh(
|
for generation: Int,
|
replayDebugCommands: Bool = true,
|
delay: TimeInterval = 0
|
) {
|
DevelopmentDiagnostics.log("quickSwitch.snapshot.schedule", [
|
"generation": generation,
|
"visible": isVisible,
|
"replayDebugCommands": replayDebugCommands,
|
"delayMilliseconds": Int(delay * 1_000)
|
])
|
snapshotTask?.cancel()
|
let loader = snapshotLoader
|
let showStartTime = showStartTime
|
snapshotTask = Task { [weak self] in
|
guard let self, !Task.isCancelled else { return }
|
if delay > 0 {
|
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
|
guard !Task.isCancelled else { return }
|
}
|
|
let loadTask = Task.detached(priority: .userInitiated) {
|
SnapshotLoadResult.load(using: loader, showStartTime: showStartTime)
|
}
|
let result = await withTaskCancellationHandler {
|
await loadTask.value
|
} onCancel: {
|
loadTask.cancel()
|
}
|
|
guard !Task.isCancelled else { return }
|
self.applySnapshotResult(
|
result,
|
generation: generation,
|
replayDebugCommands: replayDebugCommands
|
)
|
}
|
}
|
|
private func clearSnapshotState() {
|
currentSnapshot = nil
|
sourceViewModel = nil
|
currentViewModel = nil
|
lastSnapshotError = nil
|
}
|
|
private func startSystemCriticalMonitoring() {
|
invalidateSystemCriticalMonitoring()
|
checkForSystemCriticalWindows(source: "initial")
|
guard isVisible else { return }
|
|
let monitorTimer = Timer(timeInterval: 0.20, repeats: true) { [weak self] _ in
|
Task { @MainActor [weak self] in
|
self?.checkForSystemCriticalWindows(source: "poll")
|
}
|
}
|
RunLoop.main.add(monitorTimer, forMode: .common)
|
systemCriticalMonitorTimer = monitorTimer
|
|
if let debugSystemCriticalAfter {
|
let debugTimer = Timer(timeInterval: debugSystemCriticalAfter, repeats: false) { [weak self] _ in
|
Task { @MainActor [weak self] in
|
self?.handleSystemCriticalWindows(
|
[
|
SystemCriticalWindowRecord(
|
ownerName: "SecurityAgent",
|
title: "Debug Security Confirmation",
|
processIdentifier: 0,
|
layer: Int(CGWindowLevelForKey(.mainMenuWindow)),
|
bounds: .zero
|
)
|
],
|
source: "debug"
|
)
|
}
|
}
|
RunLoop.main.add(debugTimer, forMode: .common)
|
debugSystemCriticalTimer = debugTimer
|
}
|
}
|
|
private func invalidateSystemCriticalMonitoring() {
|
systemCriticalMonitorTimer?.invalidate()
|
systemCriticalMonitorTimer = nil
|
debugSystemCriticalTimer?.invalidate()
|
debugSystemCriticalTimer = nil
|
}
|
|
private func checkForSystemCriticalWindows(source: String) {
|
guard isVisible else { return }
|
|
let criticalWindows = systemCriticalWindowDetector.visibleSystemCriticalWindows()
|
guard !criticalWindows.isEmpty else { return }
|
|
handleSystemCriticalWindows(criticalWindows, source: source)
|
}
|
|
private func handleSystemCriticalWindows(_ windows: [SystemCriticalWindowRecord], source: String) {
|
guard isVisible, !windows.isEmpty else { return }
|
|
systemCriticalDetectionCount += 1
|
lastSystemCriticalAction = "closeOverlay"
|
lastSystemCriticalMonitorSource = source
|
lastSystemCriticalWindowCount = windows.count
|
lastSystemCriticalWindowTitleHashes = windows.map { DevelopmentDiagnostics.stableFingerprint($0.title) }
|
lastSystemCriticalWindowTitleLengths = windows.map { $0.title.count }
|
lastSystemCriticalWindowTitleIsEmpty = windows.map { $0.title.isEmpty }
|
lastSystemCriticalWindowOwners = windows.map(\.ownerName)
|
lastSystemCriticalOverlayLevel = coordinator.currentLevel
|
hide(reason: .systemCriticalWindow)
|
onSnapshotUpdated?()
|
}
|
|
private func commitSelection(
|
_ selection: QuickSwitchSelection,
|
source: QuickSwitchCommitSource,
|
in viewModel: QuickSwitchViewModel? = nil
|
) {
|
DevelopmentDiagnostics.log("quickSwitch.activation.commit.start", [
|
"source": source.rawValue,
|
"appGroupIndex": selection.appGroupIndex,
|
"windowIndex": selection.windowIndex,
|
"windowID": selection.windowID
|
])
|
lastCommittedSelection = selection
|
lastCommitSource = source
|
lastActivationWindowID = selection.windowID
|
lastActivationError = nil
|
|
guard let window = window(for: selection, in: viewModel ?? currentViewModel) else {
|
lastActivationResult = .windowNotFound
|
DevelopmentDiagnostics.log("quickSwitch.activation.commit.windowNotFound", [
|
"source": source.rawValue,
|
"appGroupIndex": selection.appGroupIndex,
|
"windowIndex": selection.windowIndex,
|
"windowID": selection.windowID
|
])
|
onSnapshotUpdated?()
|
return
|
}
|
|
DevelopmentDiagnostics.log("quickSwitch.activation.commit.hideBeforeActivation", [
|
"source": source.rawValue,
|
"windowID": window.id
|
])
|
hide(reason: .userClosed)
|
|
do {
|
lastActivationResult = try windowActivationService.activate(window: window)
|
DevelopmentDiagnostics.log("quickSwitch.activation.commit.result", [
|
"source": source.rawValue,
|
"windowID": window.id,
|
"appName": window.app.name,
|
"appPID": window.app.processIdentifier,
|
"titleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
|
"titleLength": window.title.count,
|
"titleIsEmpty": window.title.isEmpty,
|
"result": lastActivationResult.map(Self.activationResultString)
|
])
|
} catch {
|
let errorSummary = DevelopmentDiagnostics.errorSummaryString(error)
|
lastActivationResult = .failed(errorSummary)
|
lastActivationError = errorSummary
|
var fields: [String: CustomStringConvertible?] = [
|
"source": source.rawValue,
|
"windowID": window.id,
|
"appName": window.app.name,
|
"titleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
|
"titleLength": window.title.count,
|
"titleIsEmpty": window.title.isEmpty
|
]
|
DevelopmentDiagnostics.errorSummaryFields(error).forEach { fields[$0.key] = $0.value }
|
DevelopmentDiagnostics.log("quickSwitch.activation.commit.error", fields)
|
}
|
|
if shouldCloseAfterActivation(lastActivationResult) {
|
DevelopmentDiagnostics.log("quickSwitch.activation.commit.closeAfterActivation", [
|
"result": lastActivationResult.map(Self.activationResultString)
|
])
|
} else {
|
DevelopmentDiagnostics.log("quickSwitch.activation.commit.refreshAfterActivation", [
|
"result": lastActivationResult.map(Self.activationResultString)
|
])
|
}
|
onSnapshotUpdated?()
|
}
|
|
private func handleSpaceLaneClick(_ spaceID: UInt64, clickCount: Int) {
|
if clickCount >= 2 {
|
if pendingLockedSpaceUnlockSpaceID == spaceID {
|
cancelPendingLockedSpaceUnlock()
|
activateLockedSpace(spaceID)
|
} else {
|
DevelopmentDiagnostics.log("quickSwitch.spaceFilter.doubleClick.ignored", [
|
"spaceID": spaceID,
|
"lockedSpaceID": lockedSpaceFilterID ?? NSNull(),
|
"pendingUnlockSpaceID": pendingLockedSpaceUnlockSpaceID ?? NSNull()
|
])
|
}
|
return
|
}
|
|
cancelPendingLockedSpaceUnlock()
|
if lockedSpaceFilterID == spaceID {
|
scheduleLockedSpaceUnlock(spaceID)
|
return
|
}
|
|
applySpaceLaneSingleClick(spaceID)
|
}
|
|
private func applySpaceLaneSingleClick(_ spaceID: UInt64) {
|
guard let sourceViewModel else {
|
lastSpaceFilterAction = "blockedNoSourceViewModel"
|
DevelopmentDiagnostics.log("quickSwitch.spaceFilter.click.blocked", [
|
"spaceID": spaceID,
|
"reason": "noSourceViewModel"
|
])
|
onSnapshotUpdated?()
|
return
|
}
|
|
if let fullscreenSelection = QuickSwitchSpaceFilterPolicy.singleFullscreenSelection(
|
inSpaceID: spaceID,
|
viewModel: sourceViewModel
|
) {
|
lockedSpaceFilterID = nil
|
selectionBeforeSpaceFilter = nil
|
lastSpaceFilterAction = "activateSingleFullscreen"
|
let visibleViewModel = projectedViewModel(from: sourceViewModel)
|
currentViewModel = visibleViewModel
|
view.applyProjected(viewModel: visibleViewModel)
|
DevelopmentDiagnostics.log("quickSwitch.spaceFilter.singleFullscreenActivate", [
|
"spaceID": spaceID,
|
"windowID": fullscreenSelection.windowID
|
])
|
commitSelection(fullscreenSelection, source: .mouse, in: sourceViewModel)
|
return
|
}
|
|
let preferredSelection: QuickSwitchSelection?
|
if lockedSpaceFilterID == spaceID {
|
lockedSpaceFilterID = nil
|
preferredSelection = selectionBeforeSpaceFilter
|
selectionBeforeSpaceFilter = nil
|
lastSpaceFilterAction = "unlock"
|
} else {
|
if lockedSpaceFilterID == nil {
|
selectionBeforeSpaceFilter = view.effectiveSelectionForProjection()
|
lastSpaceFilterAction = "lock"
|
} else {
|
lastSpaceFilterAction = "switch"
|
}
|
lockedSpaceFilterID = spaceID
|
preferredSelection = nil
|
}
|
|
let visibleViewModel = projectedViewModel(
|
from: sourceViewModel,
|
preferredSelection: preferredSelection
|
)
|
currentViewModel = visibleViewModel
|
view.applyProjected(viewModel: visibleViewModel)
|
DevelopmentDiagnostics.log("quickSwitch.spaceFilter.apply", [
|
"spaceID": spaceID,
|
"lockedSpaceID": lockedSpaceFilterID,
|
"action": lastSpaceFilterAction,
|
"visibleAppCount": visibleViewModel.appShelf.count,
|
"visibleWindowCount": visibleViewModel.windowCount
|
])
|
|
if disableScreenshotRefresh {
|
view.markScreenshotsNotRequested(for: visibleViewModel, reason: "disabledByLaunchOption")
|
screenshotTask?.cancel()
|
screenshotTask = nil
|
} else {
|
scheduleScreenshotRefresh(for: visibleViewModel, generation: sessionGeneration)
|
}
|
|
onSnapshotUpdated?()
|
}
|
|
@discardableResult
|
private func handleBackgroundClick() -> Bool {
|
guard lockedSpaceFilterID != nil else { return false }
|
|
cancelPendingLockedSpaceUnlock()
|
let unlocked = unlockSpaceFilter(action: "unlockBackground")
|
DevelopmentDiagnostics.log("quickSwitch.spaceFilter.backgroundUnlock", [
|
"unlocked": unlocked
|
])
|
return unlocked
|
}
|
|
private func scheduleLockedSpaceUnlock(_ spaceID: UInt64) {
|
pendingLockedSpaceUnlockSpaceID = spaceID
|
let delay = max(0.12, NSEvent.doubleClickInterval + 0.03)
|
DevelopmentDiagnostics.log("quickSwitch.spaceFilter.unlock.pending", [
|
"spaceID": spaceID,
|
"delay": delay
|
])
|
|
pendingLockedSpaceUnlockTask = Task { @MainActor [weak self] in
|
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
|
guard !Task.isCancelled,
|
let self,
|
self.pendingLockedSpaceUnlockSpaceID == spaceID
|
else {
|
return
|
}
|
|
self.pendingLockedSpaceUnlockSpaceID = nil
|
self.pendingLockedSpaceUnlockTask = nil
|
self.applySpaceLaneSingleClick(spaceID)
|
}
|
}
|
|
private func cancelPendingLockedSpaceUnlock() {
|
pendingLockedSpaceUnlockTask?.cancel()
|
pendingLockedSpaceUnlockTask = nil
|
pendingLockedSpaceUnlockSpaceID = nil
|
}
|
|
@discardableResult
|
private func unlockSpaceFilter(action: String) -> Bool {
|
guard let sourceViewModel, lockedSpaceFilterID != nil else {
|
return false
|
}
|
|
lockedSpaceFilterID = nil
|
let preferredSelection = selectionBeforeSpaceFilter
|
selectionBeforeSpaceFilter = nil
|
lastSpaceFilterAction = action
|
|
let visibleViewModel = projectedViewModel(
|
from: sourceViewModel,
|
preferredSelection: preferredSelection
|
)
|
currentViewModel = visibleViewModel
|
view.applyProjected(viewModel: visibleViewModel)
|
|
if disableScreenshotRefresh {
|
view.markScreenshotsNotRequested(for: visibleViewModel, reason: "disabledByLaunchOption")
|
screenshotTask?.cancel()
|
screenshotTask = nil
|
} else {
|
scheduleScreenshotRefresh(for: visibleViewModel, generation: sessionGeneration)
|
}
|
|
onSnapshotUpdated?()
|
return true
|
}
|
|
private func activateLockedSpace(_ spaceID: UInt64) {
|
guard lockedSpaceFilterID == spaceID else {
|
DevelopmentDiagnostics.log("quickSwitch.spaceFilter.activateSpace.ignored", [
|
"spaceID": spaceID,
|
"lockedSpaceID": lockedSpaceFilterID ?? NSNull()
|
])
|
return
|
}
|
|
guard let sourceViewModel else {
|
lastSpaceFilterAction = "blockedNoSourceViewModel"
|
DevelopmentDiagnostics.log("quickSwitch.spaceFilter.activateSpace.blocked", [
|
"spaceID": spaceID,
|
"reason": "noSourceViewModel"
|
])
|
onSnapshotUpdated?()
|
return
|
}
|
|
lockedSpaceFilterID = nil
|
selectionBeforeSpaceFilter = nil
|
lastSpaceFilterAction = "activateSpace"
|
lastSpaceActivationSpaceID = spaceID
|
lastSpaceActivationDidRequestFocus = nil
|
lastSpaceActivationDisplayIdentifier = nil
|
lastSpaceActivationPreviousCurrentSpaceID = nil
|
lastSpaceActivationError = nil
|
|
let visibleViewModel = projectedViewModel(from: sourceViewModel)
|
currentViewModel = visibleViewModel
|
view.applyProjected(viewModel: visibleViewModel)
|
|
DevelopmentDiagnostics.log("quickSwitch.spaceFilter.activateSpace.start", [
|
"spaceID": spaceID
|
])
|
hide(reason: .userClosed)
|
let outcome = spaceActivationService.activate(spaceID: spaceID)
|
lastSpaceActivationSpaceID = outcome.targetSpaceID
|
lastSpaceActivationDidRequestFocus = outcome.didRequestFocus
|
lastSpaceActivationDisplayIdentifier = outcome.displayIdentifier
|
lastSpaceActivationPreviousCurrentSpaceID = outcome.previousCurrentSpaceID
|
lastSpaceActivationError = outcome.error
|
DevelopmentDiagnostics.log("quickSwitch.spaceFilter.activateSpace.result", [
|
"spaceID": outcome.targetSpaceID,
|
"displayIdentifier": outcome.displayIdentifier ?? NSNull(),
|
"previousCurrentSpaceID": outcome.previousCurrentSpaceID ?? NSNull(),
|
"didRequestFocus": outcome.didRequestFocus,
|
"error": outcome.error ?? NSNull()
|
])
|
onSnapshotUpdated?()
|
}
|
|
private func requestClose(_ request: QuickSwitchCloseRequest) {
|
DevelopmentDiagnostics.log("quickSwitch.close.request.start", [
|
"targetKind": request.kindDescription,
|
"appGroupIndex": request.appGroupIndex,
|
"appName": request.appName,
|
"windowID": request.windowID,
|
"titleHash": DevelopmentDiagnostics.stableFingerprint(request.title),
|
"titleLength": request.title.count
|
])
|
lastCloseTargetKind = request.kindDescription
|
lastCloseAppGroupIndex = request.appGroupIndex
|
lastCloseAppName = request.appName
|
lastCloseWindowID = request.windowID
|
lastCloseError = nil
|
pendingCloseVerification = nil
|
view.showCloseProgress(for: request)
|
|
do {
|
let result: WindowCloseResult
|
switch request {
|
case .app:
|
result = try windowCloseService.close(app: request.app)
|
case .window:
|
guard let window = request.window else {
|
lastCloseResult = .windowNotFound
|
suppressCloseTarget(request)
|
view.clearCloseFeedback()
|
onSnapshotUpdated?()
|
return
|
}
|
result = try windowCloseService.close(window: window)
|
}
|
lastCloseResult = result
|
DevelopmentDiagnostics.log("quickSwitch.close.request.result", [
|
"targetKind": request.kindDescription,
|
"appGroupIndex": request.appGroupIndex,
|
"appName": request.appName,
|
"windowID": request.windowID,
|
"result": Self.closeResultString(result)
|
])
|
if result == .requested {
|
DevelopmentDiagnostics.log("quickSwitch.close.request.refreshAfterClose", [
|
"targetKind": request.kindDescription,
|
"appGroupIndex": request.appGroupIndex,
|
"appName": request.appName,
|
"windowID": request.windowID,
|
"optimisticRemove": false
|
])
|
pendingCloseVerification = PendingCloseVerification(
|
request: request,
|
generation: sessionGeneration,
|
attempt: 0
|
)
|
scheduleSnapshotRefresh(
|
for: sessionGeneration,
|
replayDebugCommands: false,
|
delay: Self.closeVerificationRetryDelays[0]
|
)
|
} else if result == .windowNotFound {
|
suppressCloseTarget(request)
|
view.clearCloseFeedback()
|
DevelopmentDiagnostics.log("quickSwitch.close.request.staleTargetRemoved", [
|
"targetKind": request.kindDescription,
|
"appGroupIndex": request.appGroupIndex,
|
"appName": request.appName,
|
"windowID": request.windowID
|
])
|
scheduleSnapshotRefresh(
|
for: sessionGeneration,
|
replayDebugCommands: false,
|
delay: 0.10
|
)
|
} else {
|
view.showCloseFailure(message: closeFailureMessage(for: result), for: request)
|
}
|
} catch {
|
let errorSummary = DevelopmentDiagnostics.errorSummaryString(error)
|
lastCloseResult = .failed(errorSummary)
|
lastCloseError = errorSummary
|
pendingCloseVerification = nil
|
view.showCloseFailure(message: Self.closeFailedMessage, for: request)
|
var fields: [String: CustomStringConvertible?] = [
|
"targetKind": request.kindDescription,
|
"appGroupIndex": request.appGroupIndex,
|
"appName": request.appName,
|
"windowID": request.windowID,
|
"titleHash": DevelopmentDiagnostics.stableFingerprint(request.title),
|
"titleLength": request.title.count
|
]
|
DevelopmentDiagnostics.errorSummaryFields(error).forEach { fields[$0.key] = $0.value }
|
DevelopmentDiagnostics.log("quickSwitch.close.request.error", fields)
|
}
|
|
onSnapshotUpdated?()
|
}
|
|
private func suppressCloseTarget(_ request: QuickSwitchCloseRequest) {
|
let target = SuppressedCloseTarget(request)
|
if !suppressedCloseTargets.contains(target) {
|
suppressedCloseTargets.append(target)
|
}
|
|
guard let sourceViewModel else { return }
|
let visibleViewModel = projectedViewModel(from: sourceViewModel, preferredSelection: view.effectiveSelectionForProjection())
|
self.currentViewModel = visibleViewModel
|
view.applyProjected(viewModel: visibleViewModel)
|
DevelopmentDiagnostics.log("quickSwitch.close.optimisticRemove", [
|
"targetKind": request.kindDescription,
|
"appGroupIndex": request.appGroupIndex,
|
"appName": request.appName,
|
"windowID": request.windowID,
|
"remainingWindowCount": visibleViewModel.windowCount,
|
"suppressedCloseTargetCount": suppressedCloseTargets.count
|
])
|
}
|
|
private func viewModel(
|
_ viewModel: QuickSwitchViewModel,
|
excluding suppressedTargets: [SuppressedCloseTarget]
|
) -> QuickSwitchViewModel {
|
guard !suppressedTargets.isEmpty else { return viewModel }
|
|
var nextGlobalIndex = 0
|
let columns = viewModel.waterfallColumns.compactMap { column -> QuickSwitchWaterfallColumnViewModel? in
|
let windows = column.windows.filter { card in
|
!suppressedTargets.contains { target in
|
suppressedTarget(target, matches: card)
|
}
|
}
|
guard !windows.isEmpty else { return nil }
|
|
let reindexedWindows = windows.enumerated().map { windowIndex, card in
|
defer { nextGlobalIndex += 1 }
|
return QuickSwitchWindowCardViewModel(
|
appGroupIndex: column.appGroupIndex,
|
windowIndex: windowIndex,
|
globalIndex: nextGlobalIndex,
|
window: card.window,
|
primarySpaceID: card.primarySpaceID,
|
primarySpaceLabel: card.primarySpaceLabel
|
)
|
}
|
|
return QuickSwitchWaterfallColumnViewModel(
|
appGroupIndex: column.appGroupIndex,
|
app: column.app,
|
windows: reindexedWindows
|
)
|
}
|
|
let columnsByAppGroupIndex = Dictionary(uniqueKeysWithValues: columns.map { ($0.appGroupIndex, $0) })
|
let appShelf = viewModel.appShelf.compactMap { item -> QuickSwitchAppShelfItemViewModel? in
|
guard let column = columnsByAppGroupIndex[item.appGroupIndex] else { return nil }
|
return QuickSwitchAppShelfItemViewModel(
|
appGroupIndex: item.appGroupIndex,
|
app: item.app,
|
windowCount: column.windows.count,
|
primarySpaceIDs: orderedPrimarySpaceIDs(in: column.windows),
|
hasMinimizedWindows: column.windows.contains { $0.window.isMinimized },
|
hasFullscreenWindows: column.windows.contains { $0.window.isFullscreen }
|
)
|
}
|
|
return QuickSwitchViewModel(
|
displays: viewModel.displays,
|
appShelf: appShelf,
|
waterfallColumns: columns,
|
initialSelection: initialSelection(in: columns, fallback: viewModel.initialSelection),
|
lockedSpaceID: viewModel.lockedSpaceID
|
)
|
}
|
|
private func projectedViewModel(
|
from sourceViewModel: QuickSwitchViewModel,
|
preferredSelection: QuickSwitchSelection? = nil
|
) -> QuickSwitchViewModel {
|
let closeProjectedViewModel = viewModel(sourceViewModel, excluding: suppressedCloseTargets)
|
return QuickSwitchSpaceFilterPolicy.projectedViewModel(
|
from: closeProjectedViewModel,
|
lockedSpaceID: lockedSpaceFilterID,
|
preferredSelection: preferredSelection
|
)
|
}
|
|
private func orderedPrimarySpaceIDs(in cards: [QuickSwitchWindowCardViewModel]) -> [UInt64] {
|
var seen = Set<UInt64>()
|
var result: [UInt64] = []
|
for card in cards {
|
guard let spaceID = card.primarySpaceID,
|
!seen.contains(spaceID)
|
else {
|
continue
|
}
|
seen.insert(spaceID)
|
result.append(spaceID)
|
}
|
return result
|
}
|
|
private func suppressedTarget(
|
_ target: SuppressedCloseTarget,
|
matches card: QuickSwitchWindowCardViewModel
|
) -> Bool {
|
switch target {
|
case .window(let windowID):
|
return card.window.id == windowID
|
case .app(let app):
|
return appMatches(card.window.app, app)
|
}
|
}
|
|
private func initialSelection(
|
in columns: [QuickSwitchWaterfallColumnViewModel],
|
fallback: QuickSwitchSelection?
|
) -> QuickSwitchSelection? {
|
if let fallback,
|
columns.contains(where: { column in
|
column.appGroupIndex == fallback.appGroupIndex
|
&& column.windows.contains { $0.window.id == fallback.windowID }
|
}) {
|
return fallback
|
}
|
|
guard let firstCard = columns.first?.windows.first else { return nil }
|
return QuickSwitchSelection(
|
appGroupIndex: firstCard.appGroupIndex,
|
windowIndex: firstCard.windowIndex,
|
windowID: firstCard.window.id
|
)
|
}
|
|
private func shouldCloseAfterActivation(_ result: WindowActivationResult?) -> Bool {
|
switch result {
|
case .activated, .restored, .appActivatedOnly:
|
return true
|
case .windowNotFound, .unsupported, .activationFailed, .failed, nil:
|
return false
|
}
|
}
|
|
private func window(
|
for selection: QuickSwitchSelection,
|
in viewModel: QuickSwitchViewModel?
|
) -> AlignerWindow? {
|
viewModel?
|
.waterfallColumns
|
.flatMap(\.windows)
|
.first { card in
|
card.appGroupIndex == selection.appGroupIndex
|
&& card.windowIndex == selection.windowIndex
|
&& card.window.id == selection.windowID
|
}?
|
.window
|
}
|
|
private func applySnapshotResult(
|
_ result: SnapshotLoadResult,
|
generation: Int,
|
replayDebugCommands: Bool
|
) {
|
guard generation == sessionGeneration, isVisible else {
|
DevelopmentDiagnostics.log("quickSwitch.snapshot.drop", [
|
"resultGeneration": generation,
|
"currentGeneration": sessionGeneration,
|
"visible": isVisible
|
])
|
return
|
}
|
|
switch result {
|
case .success(let success):
|
currentSnapshot = success.snapshot
|
sourceViewModel = success.viewModel
|
let visibleViewModel = projectedViewModel(
|
from: success.viewModel,
|
preferredSelection: view.effectiveSelectionForProjection()
|
)
|
currentViewModel = visibleViewModel
|
lastSnapshotError = nil
|
snapshotStartElapsedMilliseconds = success.timing.startElapsedMilliseconds
|
snapshotDurationMilliseconds = success.timing.durationMilliseconds
|
snapshotRanOnMainThread = success.timing.ranOnMainThread
|
view.apply(viewModel: visibleViewModel)
|
view.recordPerformanceFirstFrameIfNeeded()
|
if replayDebugCommands {
|
view.performDebugKeyboardCommands(debugKeySequence)
|
view.performDebugMouseCommands(debugMouseSequence)
|
}
|
verifyPendingCloseIfNeeded(in: success.viewModel)
|
DevelopmentDiagnostics.log("quickSwitch.snapshot.success", [
|
"generation": generation,
|
"displayCount": success.viewModel.displays.count,
|
"spaceCount": success.viewModel.spaceCount,
|
"appCount": success.viewModel.appShelf.count,
|
"windowCount": success.viewModel.windowCount,
|
"durationMilliseconds": success.timing.durationMilliseconds,
|
"ranOnMainThread": success.timing.ranOnMainThread,
|
"initialSelectionWindowID": success.viewModel.initialSelection?.windowID
|
])
|
guard isVisible else { return }
|
if disableScreenshotRefresh {
|
view.markScreenshotsNotRequested(for: visibleViewModel, reason: "disabledByLaunchOption")
|
screenshotTask?.cancel()
|
screenshotTask = nil
|
DevelopmentDiagnostics.log("quickSwitch.screenshot.disabledByLaunchOption", [
|
"generation": generation
|
])
|
} else {
|
scheduleScreenshotRefresh(for: visibleViewModel, generation: generation)
|
}
|
case .failure(let failure):
|
currentSnapshot = nil
|
sourceViewModel = nil
|
currentViewModel = nil
|
lastSnapshotError = failure.message
|
snapshotStartElapsedMilliseconds = failure.timing.startElapsedMilliseconds
|
snapshotDurationMilliseconds = failure.timing.durationMilliseconds
|
snapshotRanOnMainThread = failure.timing.ranOnMainThread
|
view.apply(viewModel: nil)
|
screenshotTask?.cancel()
|
screenshotTask = nil
|
failPendingCloseVerificationIfNeeded(reason: "snapshotFailure")
|
DevelopmentDiagnostics.log("quickSwitch.snapshot.failure", [
|
"generation": generation,
|
"message": failure.message,
|
"durationMilliseconds": failure.timing.durationMilliseconds,
|
"ranOnMainThread": failure.timing.ranOnMainThread
|
])
|
}
|
|
onSnapshotUpdated?()
|
}
|
|
private func verifyPendingCloseIfNeeded(in viewModel: QuickSwitchViewModel) {
|
guard let pendingCloseVerification else { return }
|
|
self.pendingCloseVerification = nil
|
let request = pendingCloseVerification.request
|
let stillPresent = closeTargetStillPresent(request, in: viewModel)
|
DevelopmentDiagnostics.log("quickSwitch.close.verify", [
|
"targetKind": request.kindDescription,
|
"appGroupIndex": request.appGroupIndex,
|
"appName": request.appName,
|
"windowID": request.windowID,
|
"requestedGeneration": pendingCloseVerification.generation,
|
"currentGeneration": sessionGeneration,
|
"attempt": pendingCloseVerification.attempt,
|
"stillPresent": stillPresent
|
])
|
|
if stillPresent {
|
let nextAttempt = pendingCloseVerification.attempt + 1
|
if nextAttempt < Self.closeVerificationRetryDelays.count {
|
self.pendingCloseVerification = PendingCloseVerification(
|
request: request,
|
generation: sessionGeneration,
|
attempt: nextAttempt
|
)
|
scheduleSnapshotRefresh(
|
for: sessionGeneration,
|
replayDebugCommands: false,
|
delay: Self.closeVerificationRetryDelays[nextAttempt]
|
)
|
} else {
|
view.showCloseFailure(message: Self.closeRequestStillPresentMessage, for: request)
|
}
|
} else {
|
view.clearCloseFeedback()
|
}
|
}
|
|
private func failPendingCloseVerificationIfNeeded(reason: String) {
|
guard let pendingCloseVerification else { return }
|
|
self.pendingCloseVerification = nil
|
let request = pendingCloseVerification.request
|
DevelopmentDiagnostics.log("quickSwitch.close.verify.failed", [
|
"targetKind": request.kindDescription,
|
"appGroupIndex": request.appGroupIndex,
|
"appName": request.appName,
|
"windowID": request.windowID,
|
"requestedGeneration": pendingCloseVerification.generation,
|
"currentGeneration": sessionGeneration,
|
"reason": reason
|
])
|
view.showCloseFailure(message: Self.closeFailedMessage, for: request)
|
}
|
|
private func closeFailureMessage(for result: WindowCloseResult) -> String {
|
switch result {
|
case .requested:
|
return Self.closeRequestStillPresentMessage
|
case .windowNotFound:
|
return Self.closeTargetNotFoundMessage
|
case .unsupported:
|
return Self.closeUnsupportedMessage
|
case .appNotFound, .failed:
|
return Self.closeFailedMessage
|
}
|
}
|
|
private func closeTargetStillPresent(
|
_ request: QuickSwitchCloseRequest,
|
in viewModel: QuickSwitchViewModel
|
) -> Bool {
|
switch request {
|
case .window:
|
guard let windowID = request.windowID else { return false }
|
return viewModel.waterfallColumns
|
.flatMap(\.windows)
|
.contains { $0.window.id == windowID }
|
case .app:
|
return viewModel.appShelf.contains { appMatches($0.app, request.app) }
|
|| viewModel.waterfallColumns.contains { appMatches($0.app, request.app) }
|
}
|
}
|
|
private func appMatches(_ candidate: AlignerApp, _ target: AlignerApp) -> Bool {
|
if let candidatePID = candidate.processIdentifier,
|
let targetPID = target.processIdentifier {
|
return candidatePID == targetPID
|
}
|
|
if !candidate.bundleIdentifier.isEmpty,
|
candidate.bundleIdentifier == target.bundleIdentifier {
|
return true
|
}
|
|
return candidate.name == target.name
|
}
|
|
private func scheduleScreenshotRefresh(
|
for viewModel: QuickSwitchViewModel,
|
generation: Int
|
) {
|
screenshotTask?.cancel()
|
let provider = screenshotProvider
|
let session = screenshotSession
|
let allWindows = viewModel.waterfallColumns.flatMap { column in
|
column.windows.map(\.window)
|
}
|
let windows = allWindows.filter(Self.shouldRequestScreenshot)
|
let skippedWindows = allWindows.filter { !Self.shouldRequestScreenshot(for: $0) }
|
DevelopmentDiagnostics.log("quickSwitch.screenshot.schedule", [
|
"generation": generation,
|
"eligibleCount": windows.count,
|
"skippedCount": skippedWindows.count
|
])
|
|
for window in skippedWindows {
|
view.markScreenshotNotRequested(for: window.id, reason: "skeletonPreferred")
|
}
|
if !skippedWindows.isEmpty {
|
onSnapshotUpdated?()
|
}
|
guard !windows.isEmpty else {
|
screenshotTask = nil
|
return
|
}
|
|
screenshotTask = Task { [weak self] in
|
guard let self else { return }
|
|
var retryWindows: [AlignerWindow] = []
|
for window in windows {
|
guard !Task.isCancelled else { return }
|
let resolution = await provider.resolvedScreenshot(for: window, in: session)
|
guard !Task.isCancelled, generation == self.sessionGeneration, self.isVisible else { return }
|
self.view.applyScreenshot(resolution, for: window.id)
|
DevelopmentDiagnostics.log("quickSwitch.screenshot.resolved", [
|
"generation": generation,
|
"windowID": window.id,
|
"appName": window.app.name,
|
"source": Self.screenshotSourceString(resolution.source)
|
])
|
self.onSnapshotUpdated?()
|
|
if case .skeletonFallback(let reason) = resolution.source,
|
Self.shouldRetryScreenshot(after: reason) {
|
retryWindows.append(window)
|
}
|
}
|
|
for window in retryWindows {
|
guard !Task.isCancelled else { return }
|
let retryResolution = await provider.resolvedScreenshot(for: window, in: session)
|
guard !Task.isCancelled, generation == self.sessionGeneration, self.isVisible else { return }
|
self.view.applyScreenshot(retryResolution, for: window.id)
|
DevelopmentDiagnostics.log("quickSwitch.screenshot.retryResolved", [
|
"generation": generation,
|
"windowID": window.id,
|
"appName": window.app.name,
|
"source": Self.screenshotSourceString(retryResolution.source)
|
])
|
self.onSnapshotUpdated?()
|
}
|
}
|
}
|
|
private static func shouldRequestScreenshot(for window: AlignerWindow) -> Bool {
|
switch ThumbnailPolicy.preferredStrategy(for: window.app.category) {
|
case .screenshotPreferred:
|
return true
|
case .skeletonPreferred, .fallbackSkeleton:
|
return false
|
}
|
}
|
|
private static func shouldRetryScreenshot(after reason: ScreenshotFallbackReason) -> Bool {
|
switch reason {
|
case .screenRecordingDenied, .syntheticWindowID, .retryLimitReached:
|
return false
|
case .captureFailed, .timedOut, .invalidCapture:
|
return true
|
}
|
}
|
|
private func elapsedSinceShowStart() -> Double? {
|
guard let showStartTime else { return nil }
|
return (CACurrentMediaTime() - showStartTime) * 1000
|
}
|
|
private func updateLifecycleWindowHighWaterMark() {
|
lifecycleMaximumOverlayWindows = max(
|
lifecycleMaximumOverlayWindows,
|
overlayWindowCount()
|
)
|
lifecycleMaximumVisibleOverlayWindows = max(
|
lifecycleMaximumVisibleOverlayWindows,
|
visibleOverlayWindowCount()
|
)
|
}
|
|
private func overlayWindowCount() -> Int {
|
NSApp.windows.filter { window in
|
window.title == Self.overlayTitle
|
}.count
|
}
|
|
private func visibleOverlayWindowCount() -> Int {
|
NSApp.windows.filter { window in
|
window.title == Self.overlayTitle && window.isVisible
|
}.count
|
}
|
|
private static func activationResultString(_ result: WindowActivationResult) -> String {
|
switch result {
|
case .activated:
|
return "activated"
|
case .restored:
|
return "restored"
|
case .appActivatedOnly:
|
return "appActivatedOnly"
|
case .windowNotFound:
|
return "windowNotFound"
|
case .unsupported:
|
return "unsupported"
|
case .activationFailed:
|
return "activationFailed"
|
case .failed(let message):
|
return "failed(\(message))"
|
}
|
}
|
|
private static func closeResultString(_ result: WindowCloseResult) -> String {
|
switch result {
|
case .requested:
|
return "requested"
|
case .windowNotFound:
|
return "windowNotFound"
|
case .appNotFound:
|
return "appNotFound"
|
case .unsupported:
|
return "unsupported"
|
case .failed(let message):
|
return "failed(\(message))"
|
}
|
}
|
|
private static func screenshotSourceString(_ source: ScreenshotResolutionSource) -> String {
|
switch source {
|
case .realScreenshot:
|
return "realScreenshot"
|
case .skeletonFallback(let reason):
|
return "skeletonFallback(\(reason))"
|
}
|
}
|
|
private static func dismissReasonString(_ reason: DismissReason) -> String {
|
switch reason {
|
case .escape:
|
return "escape"
|
case .focusLost:
|
return "focusLost"
|
case .systemCriticalWindow:
|
return "systemCriticalWindow"
|
case .timeout:
|
return "timeout"
|
case .userClosed:
|
return "userClosed"
|
}
|
}
|
|
}
|
|
private struct SnapshotLoadTiming: Sendable {
|
let startElapsedMilliseconds: Double?
|
let durationMilliseconds: Double
|
let ranOnMainThread: Bool
|
}
|
|
private struct SnapshotLoadSuccess: Sendable {
|
let snapshot: QuickSwitchSnapshot
|
let viewModel: QuickSwitchViewModel
|
let timing: SnapshotLoadTiming
|
}
|
|
private struct SnapshotLoadFailure: Sendable {
|
let message: String
|
let timing: SnapshotLoadTiming
|
}
|
|
private enum SnapshotLoadResult: Sendable {
|
case success(SnapshotLoadSuccess)
|
case failure(SnapshotLoadFailure)
|
|
static func load(
|
using loader: any QuickSwitchSnapshotLoading,
|
showStartTime: CFTimeInterval?
|
) -> SnapshotLoadResult {
|
let startedAt = CACurrentMediaTime()
|
let startedOnMainThread = Thread.isMainThread
|
|
do {
|
let load = try loader.snapshot()
|
let viewModel = QuickSwitchViewModelBuilder.viewModel(
|
from: load.snapshot,
|
currentSpaceIDs: load.currentSpaceIDs
|
)
|
return .success(SnapshotLoadSuccess(
|
snapshot: load.snapshot,
|
viewModel: viewModel,
|
timing: SnapshotLoadTiming(
|
startElapsedMilliseconds: showStartTime.map { (startedAt - $0) * 1000 },
|
durationMilliseconds: (CACurrentMediaTime() - startedAt) * 1000,
|
ranOnMainThread: startedOnMainThread
|
)
|
))
|
} catch {
|
return .failure(SnapshotLoadFailure(
|
message: DevelopmentDiagnostics.errorSummaryString(error),
|
timing: SnapshotLoadTiming(
|
startElapsedMilliseconds: showStartTime.map { (startedAt - $0) * 1000 },
|
durationMilliseconds: (CACurrentMediaTime() - startedAt) * 1000,
|
ranOnMainThread: startedOnMainThread
|
)
|
))
|
}
|
}
|
}
|
|
struct Round1QuickSwitchLaunchOptions {
|
let openQuickSwitch: Bool
|
let simulateAccessibilityDenied: Bool
|
let simulateScreenRecordingDenied: Bool
|
let autoHideAfter: TimeInterval?
|
let quitAfter: TimeInterval?
|
let reportPath: String?
|
let snapshotLoadDelay: TimeInterval?
|
let fixtureAppCount: Int?
|
let fixtureWindowsPerApp: Int?
|
let fixtureMultiDisplay: Bool
|
let fixtureCandidateFiltering: Bool
|
let fixtureActivation: Bool
|
let fixtureSpaceFilter: Bool
|
let fixtureSplitView: Bool
|
let fixtureMultiPageIdentity: Bool
|
let debugHoveredAppGroupIndex: Int?
|
let debugOverlayWidth: CGFloat?
|
let debugKeySequence: [String]
|
let debugMouseSequence: [String]
|
let debugSystemCriticalAfter: TimeInterval?
|
let debugWindowActivation: Bool
|
let debugWindowClose: Bool
|
let disableScreenshotRefresh: Bool
|
let waterfallViewMode: QuickSwitchWaterfallViewMode?
|
let lifecycleCycles: Int?
|
let lifecycleInterval: TimeInterval
|
let lifecycleVisibleDuration: TimeInterval
|
|
static func parse(arguments: [String]) -> Round1QuickSwitchLaunchOptions {
|
Round1QuickSwitchLaunchOptions(
|
openQuickSwitch: arguments.contains("--round01-open-quick-switch"),
|
simulateAccessibilityDenied: arguments.contains("--round01-simulate-accessibility-denied"),
|
simulateScreenRecordingDenied: arguments.contains("--round01-simulate-screen-recording-denied"),
|
autoHideAfter: timeInterval(for: "--round01-quick-switch-auto-hide-after", in: arguments),
|
quitAfter: timeInterval(for: "--round01-quick-switch-quit-after", in: arguments),
|
reportPath: stringValue(for: "--round01-quick-switch-report", in: arguments),
|
snapshotLoadDelay: timeInterval(for: "--round01-snapshot-load-delay", in: arguments),
|
fixtureAppCount: intValue(for: "--round01-fixture-app-count", in: arguments),
|
fixtureWindowsPerApp: intValue(for: "--round01-fixture-windows-per-app", in: arguments),
|
fixtureMultiDisplay: arguments.contains("--round01-fixture-multi-display"),
|
fixtureCandidateFiltering: arguments.contains("--round01-fixture-candidate-filtering"),
|
fixtureActivation: arguments.contains("--round01-fixture-window-activation"),
|
fixtureSpaceFilter: arguments.contains("--round01-fixture-space-filter"),
|
fixtureSplitView: arguments.contains("--round01-fixture-split-view"),
|
fixtureMultiPageIdentity: arguments.contains("--round01-fixture-multi-page-identity"),
|
debugHoveredAppGroupIndex: intValue(for: "--round01-debug-hover-app-index", in: arguments),
|
debugOverlayWidth: cgFloatValue(for: "--round01-debug-overlay-width", in: arguments),
|
debugKeySequence: stringListValue(for: "--round01-debug-key-sequence", in: arguments),
|
debugMouseSequence: stringListValue(for: "--round01-debug-mouse-sequence", in: arguments),
|
debugSystemCriticalAfter: timeInterval(for: "--round01-debug-system-critical-after", in: arguments),
|
debugWindowActivation: arguments.contains("--round01-debug-window-activation"),
|
debugWindowClose: arguments.contains("--round01-debug-window-close"),
|
disableScreenshotRefresh: arguments.contains("--round01-disable-screenshot-refresh"),
|
waterfallViewMode: waterfallViewModeValue(for: "--round01-waterfall-view-mode", in: arguments),
|
lifecycleCycles: intValue(for: "--round01-quick-switch-lifecycle-cycles", in: arguments),
|
lifecycleInterval: timeInterval(for: "--round01-quick-switch-lifecycle-interval", in: arguments) ?? 0.02,
|
lifecycleVisibleDuration: timeInterval(for: "--round01-quick-switch-lifecycle-visible-duration", in: arguments) ?? 0.16
|
)
|
}
|
|
private static func timeInterval(for key: String, in arguments: [String]) -> TimeInterval? {
|
let prefix = "\(key)="
|
guard let argument = arguments.first(where: { $0.hasPrefix(prefix) }) else {
|
return nil
|
}
|
|
return TimeInterval(argument.dropFirst(prefix.count))
|
}
|
|
private static func stringValue(for key: String, in arguments: [String]) -> String? {
|
let prefix = "\(key)="
|
guard let argument = arguments.first(where: { $0.hasPrefix(prefix) }) else {
|
return nil
|
}
|
|
return String(argument.dropFirst(prefix.count))
|
}
|
|
private static func waterfallViewModeValue(
|
for key: String,
|
in arguments: [String]
|
) -> QuickSwitchWaterfallViewMode? {
|
guard let value = stringValue(for: key, in: arguments) else { return nil }
|
|
switch value {
|
case "vertical", "vertical-columns", "verticalColumns":
|
return .verticalColumns
|
case "horizontal", "horizontal-masonry", "horizontalMasonry":
|
return .horizontalMasonry
|
default:
|
return nil
|
}
|
}
|
|
private static func intValue(for key: String, in arguments: [String]) -> Int? {
|
stringValue(for: key, in: arguments).flatMap(Int.init)
|
}
|
|
private static func cgFloatValue(for key: String, in arguments: [String]) -> CGFloat? {
|
stringValue(for: key, in: arguments)
|
.flatMap(Double.init)
|
.map { CGFloat($0) }
|
}
|
|
private static func stringListValue(for key: String, in arguments: [String]) -> [String] {
|
guard let value = stringValue(for: key, in: arguments) else { return [] }
|
|
return value
|
.split(separator: ",")
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
.filter { !$0.isEmpty }
|
}
|
}
|
|
@MainActor
|
private final class StderrScreenshotDebugLogger: ScreenshotDebugLogging {
|
func record(_ event: ScreenshotDebugEvent) {
|
DevelopmentDiagnostics.log("quickSwitch.screenshot.debug", [
|
"event": event.diagnosticDescription
|
])
|
fputs("Round01 screenshot debug: \(event.diagnosticDescription)\n", stderr)
|
}
|
}
|