| | |
| | | @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 windowCloseService: any WindowCloseServiceProtocol |
| | | private let systemCriticalWindowDetector: any SystemCriticalWindowDetecting |
| | | private let disableScreenshotRefresh: Bool |
| | | private var closeConfirmationRequired: Bool |
| | | private let onCloseConfirmationDisabled: (() -> Void)? |
| | | private let debugOverlayWidth: CGFloat? |
| | | private let debugKeySequence: [String] |
| | | private let debugMouseSequence: [String] |
| | |
| | | private var lastActivationWindowID: UInt32? |
| | | private var lastActivationResult: WindowActivationResult? |
| | | private var lastActivationError: 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 lifecycleResidualVisibleOverlayWindows = 0 |
| | | var onSnapshotUpdated: (() -> 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( |
| | |
| | | windowActivationService: any WindowActivationServiceProtocol = CGWindowAXWindowService( |
| | | spaceIDsByWindowIDProvider: { _ in [:] } |
| | | ), |
| | | windowCloseService: any WindowCloseServiceProtocol = CGWindowAXWindowService( |
| | | spaceIDsByWindowIDProvider: { _ in [:] } |
| | | ), |
| | | systemCriticalWindowDetector: any SystemCriticalWindowDetecting = CGWindowSystemCriticalWindowDetector(), |
| | | disableScreenshotRefresh: Bool = false, |
| | | closeConfirmationRequired: Bool = true, |
| | | onCloseConfirmationDisabled: (() -> Void)? = nil, |
| | | debugHoveredAppGroupIndex: Int? = nil, |
| | | debugOverlayWidth: CGFloat? = nil, |
| | | debugKeySequence: [String] = [], |
| | |
| | | self.snapshotLoader = snapshotLoader |
| | | self.screenshotProvider = screenshotProvider |
| | | self.windowActivationService = windowActivationService |
| | | self.windowCloseService = windowCloseService |
| | | self.systemCriticalWindowDetector = systemCriticalWindowDetector |
| | | self.disableScreenshotRefresh = disableScreenshotRefresh |
| | | self.closeConfirmationRequired = closeConfirmationRequired |
| | | self.onCloseConfirmationDisabled = onCloseConfirmationDisabled |
| | | self.debugOverlayWidth = debugOverlayWidth |
| | | self.debugKeySequence = debugKeySequence |
| | | self.debugMouseSequence = debugMouseSequence |
| | |
| | | lastActivationWindowID = nil |
| | | lastActivationResult = nil |
| | | lastActivationError = nil |
| | | lastCloseTargetKind = nil |
| | | lastCloseAppGroupIndex = nil |
| | | lastCloseAppName = nil |
| | | lastCloseWindowID = nil |
| | | lastCloseResult = nil |
| | | lastCloseError = nil |
| | | pendingCloseVerification = nil |
| | | suppressedCloseTargets = [] |
| | | lastDismissReason = nil |
| | | lastSystemCriticalAction = nil |
| | | lastSystemCriticalMonitorSource = nil |
| | |
| | | view.onCommitSelection = { [weak self] selection, source in |
| | | self?.commitSelection(selection, source: source) |
| | | } |
| | | 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.apply(viewModel: nil) |
| | | view.beginPerformanceFirstFrameMeasurement() |
| | | coordinator.openQuickSwitch() |
| | |
| | | snapshotTask = nil |
| | | screenshotTask?.cancel() |
| | | screenshotTask = nil |
| | | pendingCloseVerification = nil |
| | | view.clearCloseFeedback() |
| | | invalidateSystemCriticalMonitoring() |
| | | coordinator.closeQuickSwitch(reason: reason) |
| | | lifecycleResidualOverlayWindows = overlayWindowCount() |
| | |
| | | "lastActivationWindowID": lastActivationWindowID ?? NSNull(), |
| | | "lastActivationResult": lastActivationResult.map(Self.activationResultString) ?? NSNull(), |
| | | "lastActivationError": lastActivationError ?? 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)), |
| | |
| | | updateLifecycleWindowHighWaterMark() |
| | | } |
| | | |
| | | private func scheduleSnapshotRefresh(for generation: Int) { |
| | | private func scheduleSnapshotRefresh( |
| | | for generation: Int, |
| | | replayDebugCommands: Bool = true, |
| | | delay: TimeInterval = 0 |
| | | ) { |
| | | DevelopmentDiagnostics.log("quickSwitch.snapshot.schedule", [ |
| | | "generation": generation, |
| | | "visible": isVisible |
| | | "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) |
| | |
| | | } |
| | | |
| | | guard !Task.isCancelled else { return } |
| | | self.applySnapshotResult(result, generation: generation) |
| | | self.applySnapshotResult( |
| | | result, |
| | | generation: generation, |
| | | replayDebugCommands: replayDebugCommands |
| | | ) |
| | | } |
| | | } |
| | | |
| | |
| | | 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 { |
| | | suppressCloseTarget(request) |
| | | DevelopmentDiagnostics.log("quickSwitch.close.request.refreshAfterClose", [ |
| | | "targetKind": request.kindDescription, |
| | | "appGroupIndex": request.appGroupIndex, |
| | | "appName": request.appName, |
| | | "windowID": request.windowID |
| | | ]) |
| | | 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 currentViewModel else { return } |
| | | let visibleViewModel = viewModel(currentViewModel, excluding: suppressedCloseTargets) |
| | | self.currentViewModel = visibleViewModel |
| | | view.apply(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) |
| | | ) |
| | | } |
| | | |
| | | 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: |
| | |
| | | .window |
| | | } |
| | | |
| | | private func applySnapshotResult(_ result: SnapshotLoadResult, generation: Int) { |
| | | private func applySnapshotResult( |
| | | _ result: SnapshotLoadResult, |
| | | generation: Int, |
| | | replayDebugCommands: Bool |
| | | ) { |
| | | guard generation == sessionGeneration, isVisible else { |
| | | DevelopmentDiagnostics.log("quickSwitch.snapshot.drop", [ |
| | | "resultGeneration": generation, |
| | |
| | | switch result { |
| | | case .success(let success): |
| | | currentSnapshot = success.snapshot |
| | | currentViewModel = success.viewModel |
| | | let visibleViewModel = viewModel(success.viewModel, excluding: suppressedCloseTargets) |
| | | currentViewModel = visibleViewModel |
| | | lastSnapshotError = nil |
| | | snapshotStartElapsedMilliseconds = success.timing.startElapsedMilliseconds |
| | | snapshotDurationMilliseconds = success.timing.durationMilliseconds |
| | | snapshotRanOnMainThread = success.timing.ranOnMainThread |
| | | view.apply(viewModel: success.viewModel) |
| | | view.apply(viewModel: visibleViewModel) |
| | | view.recordPerformanceFirstFrameIfNeeded() |
| | | view.performDebugKeyboardCommands(debugKeySequence) |
| | | view.performDebugMouseCommands(debugMouseSequence) |
| | | if replayDebugCommands { |
| | | view.performDebugKeyboardCommands(debugKeySequence) |
| | | view.performDebugMouseCommands(debugMouseSequence) |
| | | } |
| | | verifyPendingCloseIfNeeded(in: success.viewModel) |
| | | DevelopmentDiagnostics.log("quickSwitch.snapshot.success", [ |
| | | "generation": generation, |
| | | "displayCount": success.viewModel.displays.count, |
| | |
| | | ]) |
| | | guard isVisible else { return } |
| | | if disableScreenshotRefresh { |
| | | view.markScreenshotsNotRequested(for: success.viewModel, reason: "disabledByLaunchOption") |
| | | view.markScreenshotsNotRequested(for: visibleViewModel, reason: "disabledByLaunchOption") |
| | | screenshotTask?.cancel() |
| | | screenshotTask = nil |
| | | DevelopmentDiagnostics.log("quickSwitch.screenshot.disabledByLaunchOption", [ |
| | | "generation": generation |
| | | ]) |
| | | } else { |
| | | scheduleScreenshotRefresh(for: success.viewModel, generation: generation) |
| | | scheduleScreenshotRefresh(for: visibleViewModel, generation: generation) |
| | | } |
| | | case .failure(let failure): |
| | | currentSnapshot = nil |
| | |
| | | view.apply(viewModel: nil) |
| | | screenshotTask?.cancel() |
| | | screenshotTask = nil |
| | | failPendingCloseVerificationIfNeeded(reason: "snapshotFailure") |
| | | DevelopmentDiagnostics.log("quickSwitch.snapshot.failure", [ |
| | | "generation": generation, |
| | | "message": failure.message, |
| | |
| | | } |
| | | |
| | | 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( |
| | |
| | | } |
| | | } |
| | | |
| | | 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: |
| | |
| | | let snapshotLoadDelay: TimeInterval? |
| | | let fixtureAppCount: Int? |
| | | let fixtureWindowsPerApp: Int? |
| | | let fixtureMultiDisplay: Bool |
| | | let fixtureCandidateFiltering: Bool |
| | | let fixtureActivation: Bool |
| | | let debugHoveredAppGroupIndex: Int? |
| | |
| | | let debugMouseSequence: [String] |
| | | let debugSystemCriticalAfter: TimeInterval? |
| | | let debugWindowActivation: Bool |
| | | let debugWindowClose: Bool |
| | | let disableScreenshotRefresh: Bool |
| | | let lifecycleCycles: Int? |
| | | let lifecycleInterval: TimeInterval |
| | |
| | | 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"), |
| | | debugHoveredAppGroupIndex: intValue(for: "--round01-debug-hover-app-index", 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"), |
| | | lifecycleCycles: intValue(for: "--round01-quick-switch-lifecycle-cycles", in: arguments), |
| | | lifecycleInterval: timeInterval(for: "--round01-quick-switch-lifecycle-interval", in: arguments) ?? 0.02, |