| | |
| | | let flags: CGEventFlags |
| | | } |
| | | |
| | | private struct FinderTabActivationBudget { |
| | | let startedAt: TimeInterval |
| | | let limit: TimeInterval |
| | | |
| | | init(limit: TimeInterval) { |
| | | startedAt = ProcessInfo.processInfo.systemUptime |
| | | self.limit = limit |
| | | } |
| | | |
| | | var elapsed: TimeInterval { |
| | | ProcessInfo.processInfo.systemUptime - startedAt |
| | | } |
| | | |
| | | var elapsedMilliseconds: Int { |
| | | CGWindowAXWindowService.milliseconds(elapsed) |
| | | } |
| | | |
| | | var remaining: TimeInterval { |
| | | max(0, limit - elapsed) |
| | | } |
| | | |
| | | var isExhausted: Bool { |
| | | remaining <= 0 |
| | | } |
| | | } |
| | | |
| | | private struct WindowFocusAttempt { |
| | | let focused: AXActionResult |
| | | let main: AXActionResult |
| | | let raised: AXActionResult |
| | | |
| | | var success: Bool { |
| | | focused.success || main.success || raised.success |
| | | } |
| | | } |
| | | |
| | | private static let finderTabActivationBudgetLimit: TimeInterval = 1.50 |
| | | private static let finderTabCycleMaximumAttempts = 5 |
| | | private static let finderTabHostRepresentationScanLimit = 5 |
| | | private static let finderTabFastTitleMatchAttempts = 4 |
| | | private static let finderTabFastTitleMatchDelay: TimeInterval = 0.03 |
| | | private static let finderTabWindowMenuVerificationInitialDelay: TimeInterval = 0.08 |
| | | private static let finderTabWindowMenuVerificationAttempts = 8 |
| | | private static let finderTabWindowMenuVerificationDelay: TimeInterval = 0.04 |
| | | |
| | | init( |
| | | spaceIDsByWindowIDProvider: @escaping ([UInt32]) -> [UInt32: [UInt64]], |
| | | appCategorizer: any AppCategorizer = BundleIDAppCategorizer(), |
| | |
| | | throw AXWindowRestoreError.setMinimizedFailed(minimizeResult) |
| | | } |
| | | |
| | | let focusedBeforeActivateResult = focusWindowResult( |
| | | let beforeActivateAttempt = focusMainRaiseWindowResult( |
| | | restorableWindow.element, |
| | | processIdentifier: restorableWindow.processIdentifier |
| | | ) |
| | | let raisedBeforeActivateResult = raiseWindowResult(restorableWindow.element) |
| | | let focusedBeforeActivate = focusedBeforeActivateResult.success |
| | | let raisedBeforeActivate = raisedBeforeActivateResult.success |
| | | |
| | | guard activateApplication(processIdentifier: restorableWindow.processIdentifier) else { |
| | | DevelopmentDiagnostics.log("windowActivation.restore.activateApplicationFailed", [ |
| | | "windowID": windowID, |
| | | "pid": restorableWindow.processIdentifier, |
| | | "focusedBeforeActivate": focusedBeforeActivate, |
| | | "focusBeforeAXError": String(describing: focusedBeforeActivateResult.error), |
| | | "raisedBeforeActivate": raisedBeforeActivate, |
| | | "raiseBeforeAXError": String(describing: raisedBeforeActivateResult.error) |
| | | "focusedBeforeActivate": beforeActivateAttempt.focused.success, |
| | | "focusBeforeAXError": String(describing: beforeActivateAttempt.focused.error), |
| | | "mainBeforeActivate": beforeActivateAttempt.main.success, |
| | | "mainBeforeAXError": String(describing: beforeActivateAttempt.main.error), |
| | | "raisedBeforeActivate": beforeActivateAttempt.raised.success, |
| | | "raiseBeforeAXError": String(describing: beforeActivateAttempt.raised.error) |
| | | ]) |
| | | return .activationFailed |
| | | } |
| | | |
| | | let focusedAfterActivateResult = focusWindowResult( |
| | | let afterActivateAttempt = focusMainRaiseWindowResult( |
| | | restorableWindow.element, |
| | | processIdentifier: restorableWindow.processIdentifier |
| | | ) |
| | | let raisedAfterActivateResult = raiseWindowResult(restorableWindow.element) |
| | | let focusedAfterActivate = focusedAfterActivateResult.success |
| | | let raisedAfterActivate = raisedAfterActivateResult.success |
| | | |
| | | let restored = focusedBeforeActivate || focusedAfterActivate || raisedBeforeActivate || raisedAfterActivate |
| | | let restored = beforeActivateAttempt.success || afterActivateAttempt.success |
| | | DevelopmentDiagnostics.log("windowActivation.restore.end", [ |
| | | "windowID": windowID, |
| | | "pid": restorableWindow.processIdentifier, |
| | | "focusedBeforeActivate": focusedBeforeActivate, |
| | | "focusBeforeAXError": String(describing: focusedBeforeActivateResult.error), |
| | | "raisedBeforeActivate": raisedBeforeActivate, |
| | | "raiseBeforeAXError": String(describing: raisedBeforeActivateResult.error), |
| | | "focusedAfterActivate": focusedAfterActivate, |
| | | "focusAfterAXError": String(describing: focusedAfterActivateResult.error), |
| | | "raisedAfterActivate": raisedAfterActivate, |
| | | "raiseAfterAXError": String(describing: raisedAfterActivateResult.error), |
| | | "focusedBeforeActivate": beforeActivateAttempt.focused.success, |
| | | "focusBeforeAXError": String(describing: beforeActivateAttempt.focused.error), |
| | | "mainBeforeActivate": beforeActivateAttempt.main.success, |
| | | "mainBeforeAXError": String(describing: beforeActivateAttempt.main.error), |
| | | "raisedBeforeActivate": beforeActivateAttempt.raised.success, |
| | | "raiseBeforeAXError": String(describing: beforeActivateAttempt.raised.error), |
| | | "focusedAfterActivate": afterActivateAttempt.focused.success, |
| | | "focusAfterAXError": String(describing: afterActivateAttempt.focused.error), |
| | | "mainAfterActivate": afterActivateAttempt.main.success, |
| | | "mainAfterAXError": String(describing: afterActivateAttempt.main.error), |
| | | "raisedAfterActivate": afterActivateAttempt.raised.success, |
| | | "raiseAfterAXError": String(describing: afterActivateAttempt.raised.error), |
| | | "result": restored ? "restored" : "activationFailed" |
| | | ]) |
| | | return focusedBeforeActivate || focusedAfterActivate || raisedBeforeActivate || raisedAfterActivate |
| | | ? .restored |
| | | : .activationFailed |
| | | return restored ? .restored : .activationFailed |
| | | } |
| | | |
| | | func activate(window requestedWindow: AlignerWindow) throws -> WindowActivationResult { |
| | | let window = refreshedFinderWindowForPreciseAction(requestedWindow, operation: "windowActivation") |
| | | func activate(window: AlignerWindow) throws -> WindowActivationResult { |
| | | DevelopmentDiagnostics.log("windowActivation.activate.start", [ |
| | | "windowID": window.id, |
| | | "identifierSource": String(describing: window.identifierSource), |
| | |
| | | } |
| | | |
| | | let activated = activateApplication(for: window) |
| | | let retryAttempt = activated |
| | | ? retryFocusMainRaiseWindow( |
| | | for: window, |
| | | delay: 0.10, |
| | | matchAttempt: "axMissingPostActivate" |
| | | ) |
| | | : nil |
| | | let retrySucceeded = retryAttempt?.success ?? false |
| | | DevelopmentDiagnostics.log("windowActivation.activate.axMissing", [ |
| | | "windowID": window.id, |
| | | "appName": window.app.name, |
| | | "pid": window.app.processIdentifier, |
| | | "privateActivated": didPrivatelyActivate, |
| | | "applicationActivated": activated, |
| | | "result": didPrivatelyActivate ? "activated" : (activated ? "appActivatedOnly" : "windowNotFound") |
| | | "retryFocused": retryAttempt?.focused.success, |
| | | "retryFocusAXError": retryAttempt.map { String(describing: $0.focused.error) }, |
| | | "retryMain": retryAttempt?.main.success, |
| | | "retryMainAXError": retryAttempt.map { String(describing: $0.main.error) }, |
| | | "retryRaised": retryAttempt?.raised.success, |
| | | "retryRaiseAXError": retryAttempt.map { String(describing: $0.raised.error) }, |
| | | "result": (didPrivatelyActivate || retrySucceeded) ? "activated" : (activated ? "appActivatedOnly" : "windowNotFound") |
| | | ]) |
| | | if didPrivatelyActivate { |
| | | if didPrivatelyActivate || retrySucceeded { |
| | | return .activated |
| | | } |
| | | |
| | | return activated ? .appActivatedOnly : .windowNotFound |
| | | } |
| | | |
| | | let focusedBeforeActivateResult = focusWindowResult( |
| | | let beforeActivateAttempt = focusMainRaiseWindowResult( |
| | | axWindow.element, |
| | | processIdentifier: axWindow.processIdentifier |
| | | ) |
| | | let raisedBeforeActivateResult = raiseWindowResult(axWindow.element) |
| | | let focusedBeforeActivate = focusedBeforeActivateResult.success |
| | | let raisedBeforeActivate = raisedBeforeActivateResult.success |
| | | |
| | | guard didPrivatelyActivate || activateApplication(for: window) else { |
| | | DevelopmentDiagnostics.log("windowActivation.activate.applicationActivationFailed", [ |
| | | "windowID": window.id, |
| | | "pid": window.app.processIdentifier, |
| | | "privateActivated": didPrivatelyActivate, |
| | | "focusedBeforeActivate": focusedBeforeActivate, |
| | | "focusBeforeAXError": String(describing: focusedBeforeActivateResult.error), |
| | | "raisedBeforeActivate": raisedBeforeActivate, |
| | | "raiseBeforeAXError": String(describing: raisedBeforeActivateResult.error) |
| | | "focusedBeforeActivate": beforeActivateAttempt.focused.success, |
| | | "focusBeforeAXError": String(describing: beforeActivateAttempt.focused.error), |
| | | "mainBeforeActivate": beforeActivateAttempt.main.success, |
| | | "mainBeforeAXError": String(describing: beforeActivateAttempt.main.error), |
| | | "raisedBeforeActivate": beforeActivateAttempt.raised.success, |
| | | "raiseBeforeAXError": String(describing: beforeActivateAttempt.raised.error) |
| | | ]) |
| | | return .activationFailed |
| | | } |
| | | |
| | | let focusedAfterActivateResult = focusWindowResult( |
| | | let afterActivateAttempt = focusMainRaiseWindowResult( |
| | | axWindow.element, |
| | | processIdentifier: axWindow.processIdentifier |
| | | ) |
| | | let raisedAfterActivateResult = raiseWindowResult(axWindow.element) |
| | | let focusedAfterActivate = focusedAfterActivateResult.success |
| | | let raisedAfterActivate = raisedAfterActivateResult.success |
| | | let retryAttempt = (didPrivatelyActivate || beforeActivateAttempt.success || afterActivateAttempt.success) |
| | | ? nil |
| | | : retryFocusMainRaiseWindow( |
| | | for: window, |
| | | delay: 0.10, |
| | | matchAttempt: "postActivateRetry" |
| | | ) |
| | | |
| | | let didFocusOrRaise = focusedBeforeActivate || focusedAfterActivate || raisedBeforeActivate || raisedAfterActivate |
| | | let didFocusOrRaise = beforeActivateAttempt.success || afterActivateAttempt.success || (retryAttempt?.success ?? false) |
| | | let finderDirectFallbackResult: Bool? |
| | | if (didPrivatelyActivate || didFocusOrRaise), |
| | | isFinderCGOnlyPageCandidate(window), |
| | | let processIdentifier = window.app.processIdentifier { |
| | | let normalizedTargetTitle = normalizedWindowTitle(window.title) |
| | | let focusedTitle = finderFocusedOrMainWindowTitle(processIdentifier: processIdentifier) |
| | | let directTitleMatched = normalizedWindowTitle(focusedTitle ?? "") == normalizedTargetTitle |
| | | DevelopmentDiagnostics.log("windowActivation.activate.finderDirectVerification", [ |
| | | "windowID": window.id, |
| | | "pid": processIdentifier, |
| | | "directTitleMatched": directTitleMatched, |
| | | "focusedTitleHash": DevelopmentDiagnostics.stableFingerprint(focusedTitle), |
| | | "focusedTitleLength": focusedTitle?.count, |
| | | "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title), |
| | | "targetTitleLength": window.title.count |
| | | ]) |
| | | if directTitleMatched { |
| | | finderDirectFallbackResult = true |
| | | } else { |
| | | finderDirectFallbackResult = activateFinderTabPageIfNeeded( |
| | | window: window, |
| | | axMetadata: AXWindowMetadataReader.metadata(appCategorizer: appCategorizer), |
| | | operation: "windowActivation", |
| | | reason: "directAXTitleMismatch" |
| | | ) |
| | | } |
| | | } else { |
| | | finderDirectFallbackResult = nil |
| | | } |
| | | |
| | | let finderPreciseActivationFailed = finderDirectFallbackResult == false |
| | | let activated = finderDirectFallbackResult ?? (didPrivatelyActivate || didFocusOrRaise) |
| | | DevelopmentDiagnostics.log("windowActivation.activate.end", [ |
| | | "windowID": window.id, |
| | | "pid": window.app.processIdentifier, |
| | | "privateActivated": didPrivatelyActivate, |
| | | "focusedBeforeActivate": focusedBeforeActivate, |
| | | "focusBeforeAXError": String(describing: focusedBeforeActivateResult.error), |
| | | "raisedBeforeActivate": raisedBeforeActivate, |
| | | "raiseBeforeAXError": String(describing: raisedBeforeActivateResult.error), |
| | | "focusedAfterActivate": focusedAfterActivate, |
| | | "focusAfterAXError": String(describing: focusedAfterActivateResult.error), |
| | | "raisedAfterActivate": raisedAfterActivate, |
| | | "raiseAfterAXError": String(describing: raisedAfterActivateResult.error), |
| | | "result": (didFocusOrRaise || didPrivatelyActivate) ? "activated" : "appActivatedOnly" |
| | | "focusedBeforeActivate": beforeActivateAttempt.focused.success, |
| | | "focusBeforeAXError": String(describing: beforeActivateAttempt.focused.error), |
| | | "mainBeforeActivate": beforeActivateAttempt.main.success, |
| | | "mainBeforeAXError": String(describing: beforeActivateAttempt.main.error), |
| | | "raisedBeforeActivate": beforeActivateAttempt.raised.success, |
| | | "raiseBeforeAXError": String(describing: beforeActivateAttempt.raised.error), |
| | | "focusedAfterActivate": afterActivateAttempt.focused.success, |
| | | "focusAfterAXError": String(describing: afterActivateAttempt.focused.error), |
| | | "mainAfterActivate": afterActivateAttempt.main.success, |
| | | "mainAfterAXError": String(describing: afterActivateAttempt.main.error), |
| | | "raisedAfterActivate": afterActivateAttempt.raised.success, |
| | | "raiseAfterAXError": String(describing: afterActivateAttempt.raised.error), |
| | | "retryFocused": retryAttempt?.focused.success, |
| | | "retryFocusAXError": retryAttempt.map { String(describing: $0.focused.error) }, |
| | | "retryMain": retryAttempt?.main.success, |
| | | "retryMainAXError": retryAttempt.map { String(describing: $0.main.error) }, |
| | | "retryRaised": retryAttempt?.raised.success, |
| | | "retryRaiseAXError": retryAttempt.map { String(describing: $0.raised.error) }, |
| | | "finderDirectFallbackResult": finderDirectFallbackResult, |
| | | "result": finderPreciseActivationFailed ? "activationFailed" : (activated ? "activated" : "appActivatedOnly") |
| | | ]) |
| | | return didPrivatelyActivate || focusedBeforeActivate || focusedAfterActivate || raisedBeforeActivate || raisedAfterActivate |
| | | ? .activated |
| | | : .appActivatedOnly |
| | | if finderPreciseActivationFailed { |
| | | return .activationFailed |
| | | } |
| | | return activated ? .activated : .appActivatedOnly |
| | | } |
| | | |
| | | func close(window requestedWindow: AlignerWindow) throws -> WindowCloseResult { |
| | | let window = refreshedFinderWindowForPreciseAction(requestedWindow, operation: "windowClose") |
| | | func close(window: AlignerWindow) throws -> WindowCloseResult { |
| | | DevelopmentDiagnostics.log("windowClose.window.start", [ |
| | | "windowID": window.id, |
| | | "identifierSource": String(describing: window.identifierSource), |
| | |
| | | processIdentifier: matchedAXWindow.processIdentifier, |
| | | attempt: "initial" |
| | | ) |
| | | } |
| | | |
| | | private func refreshedFinderWindowForPreciseAction( |
| | | _ window: AlignerWindow, |
| | | operation: String |
| | | ) -> AlignerWindow { |
| | | guard isFinderCGOnlyPageCandidate(window) else { return window } |
| | | |
| | | guard let refreshedWindow = try? allWindows().first(where: { refreshed in |
| | | refreshed.id == window.id |
| | | && refreshed.identifierSource == window.identifierSource |
| | | && refreshed.app.bundleIdentifier == window.app.bundleIdentifier |
| | | }) else { |
| | | DevelopmentDiagnostics.log("\(operation).finderRefresh.missing", [ |
| | | "windowID": window.id, |
| | | "spaceIDCount": window.spaceIDs.count, |
| | | "hasFrame": window.frame != nil |
| | | ]) |
| | | return window |
| | | } |
| | | |
| | | let mergedWindow = AlignerWindow( |
| | | id: window.id, |
| | | app: window.app.processIdentifier == nil ? refreshedWindow.app : window.app, |
| | | title: window.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty |
| | | ? refreshedWindow.title |
| | | : window.title, |
| | | identifierSource: window.identifierSource, |
| | | isMinimized: window.isMinimized || refreshedWindow.isMinimized, |
| | | isFullscreen: window.isFullscreen || refreshedWindow.isFullscreen, |
| | | isGhost: window.isGhost, |
| | | frame: window.frame ?? refreshedWindow.frame, |
| | | spaceIDs: window.spaceIDs.isEmpty ? refreshedWindow.spaceIDs : window.spaceIDs |
| | | ) |
| | | |
| | | if mergedWindow.spaceIDs != window.spaceIDs |
| | | || mergedWindow.isFullscreen != window.isFullscreen |
| | | || (window.frame == nil && refreshedWindow.frame != nil) { |
| | | DevelopmentDiagnostics.log("\(operation).finderRefresh.merged", [ |
| | | "windowID": window.id, |
| | | "originalSpaceIDCount": window.spaceIDs.count, |
| | | "refreshedSpaceIDCount": refreshedWindow.spaceIDs.count, |
| | | "mergedSpaceIDCount": mergedWindow.spaceIDs.count, |
| | | "originalIsFullscreen": window.isFullscreen, |
| | | "refreshedIsFullscreen": refreshedWindow.isFullscreen, |
| | | "mergedIsFullscreen": mergedWindow.isFullscreen, |
| | | "originalHasFrame": window.frame != nil, |
| | | "refreshedHasFrame": refreshedWindow.frame != nil, |
| | | "mergedHasFrame": mergedWindow.frame != nil |
| | | ]) |
| | | } |
| | | |
| | | return mergedWindow |
| | | } |
| | | |
| | | private func closeButton(for axWindow: AXWindowMetadata) -> AXUIElement? { |
| | |
| | | return nil |
| | | } |
| | | |
| | | guard let host = finderTabHostAXWindow(for: window, in: axMetadata) else { |
| | | let budget = FinderTabActivationBudget(limit: Self.finderTabActivationBudgetLimit) |
| | | let hostLookupStartedAt = ProcessInfo.processInfo.systemUptime |
| | | let host = finderTabHostAXWindow(for: window, in: axMetadata, budget: budget) |
| | | DevelopmentDiagnostics.log("\(operation).finderTab.hostLookup", [ |
| | | "windowID": window.id, |
| | | "pid": processIdentifier, |
| | | "reason": reason, |
| | | "hostFound": host != nil, |
| | | "hostWindowID": host?.windowID, |
| | | "elapsedMilliseconds": Self.elapsedMilliseconds(since: hostLookupStartedAt), |
| | | "overallElapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit), |
| | | "budgetRemainingMilliseconds": Self.milliseconds(budget.remaining) |
| | | ]) |
| | | |
| | | guard let host else { |
| | | if budget.isExhausted { |
| | | DevelopmentDiagnostics.log("\(operation).finderTab.hostMissingBudgetExceeded", [ |
| | | "windowID": window.id, |
| | | "pid": processIdentifier, |
| | | "reason": reason, |
| | | "elapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit) |
| | | ]) |
| | | return false |
| | | } |
| | | |
| | | let normalizedTargetTitle = normalizedWindowTitle(window.title) |
| | | let applicationActivated = activateApplication(processIdentifier: processIdentifier) |
| | | let selectedViaWindowMenu = !normalizedTargetTitle.isEmpty |
| | | && selectFinderWindowMenuItem( |
| | | processIdentifier: processIdentifier, |
| | | window: window, |
| | | normalizedTargetTitle: normalizedTargetTitle, |
| | | budget: budget |
| | | ) |
| | | DevelopmentDiagnostics.log("\(operation).finderTab.hostlessWindowMenuResult", [ |
| | | "windowID": window.id, |
| | | "pid": processIdentifier, |
| | | "reason": reason, |
| | | "applicationActivated": applicationActivated, |
| | | "selectedViaWindowMenu": selectedViaWindowMenu, |
| | | "spaceIDs": window.spaceIDs, |
| | | "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title), |
| | | "targetTitleLength": window.title.count, |
| | | "elapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit) |
| | | ]) |
| | | if selectedViaWindowMenu { |
| | | return true |
| | | } |
| | | |
| | | DevelopmentDiagnostics.log("\(operation).finderTab.hostMissing", [ |
| | | "windowID": window.id, |
| | | "pid": processIdentifier, |
| | | "reason": reason, |
| | | "spaceIDs": window.spaceIDs |
| | | "spaceIDs": window.spaceIDs, |
| | | "elapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit) |
| | | ]) |
| | | return false |
| | | } |
| | |
| | | } |
| | | |
| | | let applicationActivated = activateApplication(processIdentifier: processIdentifier) |
| | | let focusBeforeResult = focusWindowResult(host.element, processIdentifier: processIdentifier) |
| | | let raiseBeforeResult = raiseWindowResult(host.element) |
| | | let selectedTab = selectFinderTab(window, in: host) |
| | | Thread.sleep(forTimeInterval: 0.05) |
| | | let focusAfterResult: AXActionResult? |
| | | let raiseAfterResult: AXActionResult? |
| | | let beforeAttempt = focusMainRaiseWindowResult(host.element, processIdentifier: processIdentifier) |
| | | let selectedTabStartedAt = ProcessInfo.processInfo.systemUptime |
| | | let selectedTab = budget.isExhausted ? false : selectFinderTab(window, in: host, budget: budget) |
| | | DevelopmentDiagnostics.log("\(operation).finderTab.selectElapsed", [ |
| | | "windowID": window.id, |
| | | "pid": processIdentifier, |
| | | "hostWindowID": host.windowID, |
| | | "selectedTab": selectedTab, |
| | | "elapsedMilliseconds": Self.elapsedMilliseconds(since: selectedTabStartedAt), |
| | | "overallElapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit), |
| | | "budgetRemainingMilliseconds": Self.milliseconds(budget.remaining) |
| | | ]) |
| | | if selectedTab, budget.remaining > 0 { |
| | | Thread.sleep(forTimeInterval: min(0.05, budget.remaining)) |
| | | } |
| | | let afterAttempt: WindowFocusAttempt? |
| | | if selectedTab { |
| | | // For Finder tab/page cards, re-focusing the original host AX window after |
| | | // selecting the tab can switch Finder back to the host's previous tab. |
| | | focusAfterResult = nil |
| | | raiseAfterResult = nil |
| | | afterAttempt = nil |
| | | } else { |
| | | focusAfterResult = focusWindowResult(host.element, processIdentifier: processIdentifier) |
| | | raiseAfterResult = raiseWindowResult(host.element) |
| | | afterAttempt = focusMainRaiseWindowResult(host.element, processIdentifier: processIdentifier) |
| | | } |
| | | |
| | | let didFocusOrRaise = focusBeforeResult.success |
| | | || raiseBeforeResult.success |
| | | || (focusAfterResult?.success ?? false) |
| | | || (raiseAfterResult?.success ?? false) |
| | | let didFocusOrRaise = beforeAttempt.success || (afterAttempt?.success ?? false) |
| | | let activated = selectedTab |
| | | && (hostPrivateActivationSucceeded || applicationActivated || didFocusOrRaise) |
| | | |
| | |
| | | "hostPrivateActivated": hostPrivateActivationSucceeded, |
| | | "applicationActivated": applicationActivated, |
| | | "selectedTab": selectedTab, |
| | | "focusBeforeAXError": String(describing: focusBeforeResult.error), |
| | | "raiseBeforeAXError": String(describing: raiseBeforeResult.error), |
| | | "focusAfterAXError": focusAfterResult.map { String(describing: $0.error) } ?? "skippedAfterTabSelection", |
| | | "raiseAfterAXError": raiseAfterResult.map { String(describing: $0.error) } ?? "skippedAfterTabSelection", |
| | | "activated": activated |
| | | "focusBeforeAXError": String(describing: beforeAttempt.focused.error), |
| | | "mainBeforeAXError": String(describing: beforeAttempt.main.error), |
| | | "raiseBeforeAXError": String(describing: beforeAttempt.raised.error), |
| | | "focusAfterAXError": afterAttempt.map { String(describing: $0.focused.error) } ?? "skippedAfterTabSelection", |
| | | "mainAfterAXError": afterAttempt.map { String(describing: $0.main.error) } ?? "skippedAfterTabSelection", |
| | | "raiseAfterAXError": afterAttempt.map { String(describing: $0.raised.error) } ?? "skippedAfterTabSelection", |
| | | "activated": activated, |
| | | "elapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit), |
| | | "budgetExceeded": budget.isExhausted |
| | | ]) |
| | | |
| | | return activated |
| | |
| | | |
| | | private func finderTabHostAXWindow( |
| | | for window: AlignerWindow, |
| | | in axMetadata: [AXWindowMetadata] |
| | | in axMetadata: [AXWindowMetadata], |
| | | budget: FinderTabActivationBudget |
| | | ) -> AXWindowMetadata? { |
| | | guard let processIdentifier = window.app.processIdentifier else { return nil } |
| | | let candidates = axMetadata.filter { metadata in |
| | |
| | | return !Set(candidateSpacesByID[windowID] ?? []).isDisjoint(with: targetSpaceIDs) |
| | | } |
| | | let spaceScopedCandidates = spaceCompatibleCandidates.isEmpty ? candidates : spaceCompatibleCandidates |
| | | let selectableHostCandidates = spaceScopedCandidates.filter { metadata in |
| | | finderHostCanRepresentTarget(window, in: metadata) |
| | | } |
| | | |
| | | let hostCandidates: [AXWindowMetadata] |
| | | if targetSpaceIDs.isEmpty { |
| | | guard !selectableHostCandidates.isEmpty else { |
| | | DevelopmentDiagnostics.log("windowActivation.finderTab.noSelectableHost", [ |
| | | "windowID": window.id, |
| | | "candidateCount": candidates.count, |
| | | "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title), |
| | | "targetTitleLength": window.title.count |
| | | ]) |
| | | return nil |
| | | } |
| | | hostCandidates = selectableHostCandidates |
| | | } else { |
| | | hostCandidates = selectableHostCandidates.isEmpty ? spaceScopedCandidates : selectableHostCandidates |
| | | } |
| | | |
| | | let targetFrame = matchingFrame(for: window) |
| | | let scoredHosts = hostCandidates.compactMap { metadata -> (metadata: AXWindowMetadata, score: CGFloat)? in |
| | | let frameScoredHosts = spaceScopedCandidates.compactMap { metadata -> (metadata: AXWindowMetadata, score: CGFloat)? in |
| | | guard let score = frameOverlapScore(targetFrame, metadata.frame), |
| | | score >= 0.85 |
| | | else { |
| | |
| | | return (lhs.metadata.windowID ?? UInt32.max) < (rhs.metadata.windowID ?? UInt32.max) |
| | | } |
| | | |
| | | if let best = scoredHosts.first { |
| | | if scoredHosts.count == 1 { |
| | | if let best = frameScoredHosts.first { |
| | | if frameScoredHosts.count == 1 { |
| | | DevelopmentDiagnostics.log("windowActivation.finderTab.frameInferredHost", [ |
| | | "windowID": window.id, |
| | | "hostWindowID": best.metadata.windowID, |
| | | "score": Double(best.score), |
| | | "spaceIDCount": targetSpaceIDs.count, |
| | | "candidateCount": spaceScopedCandidates.count, |
| | | "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title), |
| | | "targetTitleLength": window.title.count |
| | | ]) |
| | | return best.metadata |
| | | } |
| | | |
| | | let secondBest = scoredHosts[1] |
| | | let secondBest = frameScoredHosts[1] |
| | | if best.score - secondBest.score >= 0.05 { |
| | | DevelopmentDiagnostics.log("windowActivation.finderTab.frameInferredHost", [ |
| | | "windowID": window.id, |
| | | "hostWindowID": best.metadata.windowID, |
| | | "score": Double(best.score), |
| | | "secondBestScore": Double(secondBest.score), |
| | | "spaceIDCount": targetSpaceIDs.count, |
| | | "candidateCount": spaceScopedCandidates.count, |
| | | "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title), |
| | | "targetTitleLength": window.title.count |
| | | ]) |
| | | return best.metadata |
| | | } |
| | | } |
| | | |
| | | guard !budget.isExhausted else { |
| | | DevelopmentDiagnostics.log("windowActivation.finderTab.hostLookupBudgetExceeded", [ |
| | | "windowID": window.id, |
| | | "candidateCount": spaceScopedCandidates.count, |
| | | "spaceIDCount": targetSpaceIDs.count, |
| | | "elapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit) |
| | | ]) |
| | | return nil |
| | | } |
| | | |
| | | let representationScanCandidates = Array(spaceScopedCandidates.prefix(Self.finderTabHostRepresentationScanLimit)) |
| | | let selectableHostCandidates = representationScanCandidates.filter { metadata in |
| | | finderHostCanRepresentTarget(window, in: metadata, budget: budget) |
| | | } |
| | | |
| | | if spaceScopedCandidates.count > representationScanCandidates.count { |
| | | DevelopmentDiagnostics.log("windowActivation.finderTab.hostRepresentationScanLimited", [ |
| | | "windowID": window.id, |
| | | "candidateCount": spaceScopedCandidates.count, |
| | | "scannedCandidateCount": representationScanCandidates.count, |
| | | "selectableCandidateCount": selectableHostCandidates.count, |
| | | "elapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit) |
| | | ]) |
| | | } |
| | | |
| | | let hostCandidates = selectableHostCandidates.isEmpty |
| | | ? spaceScopedCandidates |
| | | : selectableHostCandidates |
| | | |
| | | if hostCandidates.count == 1 { |
| | | if selectableHostCandidates.isEmpty { |
| | | DevelopmentDiagnostics.log("windowActivation.finderTab.singleFrameHostFallback", [ |
| | | "windowID": window.id, |
| | | "hostWindowID": hostCandidates[0].windowID, |
| | | "spaceIDCount": targetSpaceIDs.count, |
| | | "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title), |
| | | "targetTitleLength": window.title.count |
| | | ]) |
| | | } |
| | | return hostCandidates[0] |
| | | } |
| | | |
| | |
| | | |
| | | private func finderHostCanRepresentTarget( |
| | | _ window: AlignerWindow, |
| | | in host: AXWindowMetadata |
| | | in host: AXWindowMetadata, |
| | | budget: FinderTabActivationBudget |
| | | ) -> Bool { |
| | | let normalizedTargetTitle = normalizedWindowTitle(window.title) |
| | | guard !normalizedTargetTitle.isEmpty else { return false } |
| | |
| | | return true |
| | | } |
| | | |
| | | return finderTabControl(matchingNormalizedTitle: normalizedTargetTitle, in: host.element) != nil |
| | | guard !budget.isExhausted else { return false } |
| | | return finderTabControl(matchingNormalizedTitle: normalizedTargetTitle, in: host.element, budget: budget) != nil |
| | | } |
| | | |
| | | private func selectFinderTab( |
| | | _ window: AlignerWindow, |
| | | in host: AXWindowMetadata |
| | | in host: AXWindowMetadata, |
| | | budget: FinderTabActivationBudget |
| | | ) -> Bool { |
| | | let normalizedTargetTitle = normalizedWindowTitle(window.title) |
| | | guard !normalizedTargetTitle.isEmpty else { return false } |
| | |
| | | |
| | | guard let tabControl = finderTabControl( |
| | | matchingNormalizedTitle: normalizedTargetTitle, |
| | | in: host.element |
| | | in: host.element, |
| | | budget: budget |
| | | ) else { |
| | | DevelopmentDiagnostics.log("windowActivation.finderTab.tabButtonMissing", [ |
| | | "windowID": window.id, |
| | | "hostWindowID": host.windowID, |
| | | "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title), |
| | | "targetTitleLength": window.title.count |
| | | "targetTitleLength": window.title.count, |
| | | "elapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit) |
| | | ]) |
| | | if let processIdentifier = window.app.processIdentifier, |
| | | selectFinderWindowMenuItem( |
| | | processIdentifier: processIdentifier, |
| | | window: window, |
| | | normalizedTargetTitle: normalizedTargetTitle |
| | | ) { |
| | | return true |
| | | } |
| | | if let processIdentifier = window.app.processIdentifier, |
| | | !budget.isExhausted, |
| | | selectFinderTabByCyclingTabs( |
| | | processIdentifier: processIdentifier, |
| | | host: host, |
| | | window: window, |
| | | normalizedTargetTitle: normalizedTargetTitle |
| | | normalizedTargetTitle: normalizedTargetTitle, |
| | | budget: budget |
| | | ) { |
| | | return true |
| | | } |
| | | if let processIdentifier = window.app.processIdentifier, |
| | | !budget.isExhausted, |
| | | selectFinderWindowMenuItem( |
| | | processIdentifier: processIdentifier, |
| | | window: window, |
| | | normalizedTargetTitle: normalizedTargetTitle, |
| | | budget: budget |
| | | ) { |
| | | return true |
| | | } |
| | |
| | | "axError": String(describing: pressResult) |
| | | ]) |
| | | if pressResult == .success, |
| | | finderHostTitleMatchesTarget(host, normalizedTargetTitle: normalizedTargetTitle) { |
| | | finderHostTitleMatchesTarget( |
| | | host, |
| | | normalizedTargetTitle: normalizedTargetTitle, |
| | | attempts: Self.finderTabFastTitleMatchAttempts, |
| | | delay: Self.finderTabFastTitleMatchDelay, |
| | | budget: budget |
| | | ) { |
| | | return true |
| | | } |
| | | |
| | |
| | | "axError": String(describing: selectedChildrenResult) |
| | | ]) |
| | | if selectedChildrenResult == .success, |
| | | finderHostTitleMatchesTarget(host, normalizedTargetTitle: normalizedTargetTitle) { |
| | | finderHostTitleMatchesTarget( |
| | | host, |
| | | normalizedTargetTitle: normalizedTargetTitle, |
| | | attempts: Self.finderTabFastTitleMatchAttempts, |
| | | delay: Self.finderTabFastTitleMatchDelay, |
| | | budget: budget |
| | | ) { |
| | | return true |
| | | } |
| | | } |
| | |
| | | "axError": String(describing: valueResult) |
| | | ]) |
| | | if valueResult == .success, |
| | | finderHostTitleMatchesTarget(host, normalizedTargetTitle: normalizedTargetTitle) { |
| | | return true |
| | | } |
| | | |
| | | if let processIdentifier = window.app.processIdentifier, |
| | | selectFinderWindowMenuItem( |
| | | processIdentifier: processIdentifier, |
| | | window: window, |
| | | normalizedTargetTitle: normalizedTargetTitle |
| | | finderHostTitleMatchesTarget( |
| | | host, |
| | | normalizedTargetTitle: normalizedTargetTitle, |
| | | attempts: Self.finderTabFastTitleMatchAttempts, |
| | | delay: Self.finderTabFastTitleMatchDelay, |
| | | budget: budget |
| | | ) { |
| | | return true |
| | | } |
| | | |
| | | if let processIdentifier = window.app.processIdentifier, |
| | | !budget.isExhausted, |
| | | selectFinderTabByCyclingTabs( |
| | | processIdentifier: processIdentifier, |
| | | host: host, |
| | | window: window, |
| | | normalizedTargetTitle: normalizedTargetTitle |
| | | normalizedTargetTitle: normalizedTargetTitle, |
| | | budget: budget |
| | | ) { |
| | | return true |
| | | } |
| | | |
| | | if let processIdentifier = window.app.processIdentifier, |
| | | !budget.isExhausted, |
| | | selectFinderWindowMenuItem( |
| | | processIdentifier: processIdentifier, |
| | | window: window, |
| | | normalizedTargetTitle: normalizedTargetTitle, |
| | | budget: budget |
| | | ) { |
| | | return true |
| | | } |
| | |
| | | "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title), |
| | | "targetTitleLength": window.title.count, |
| | | "currentHostTitleHash": DevelopmentDiagnostics.stableFingerprint(currentHostTitle), |
| | | "currentHostTitleLength": currentHostTitle.count |
| | | "currentHostTitleLength": currentHostTitle.count, |
| | | "elapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit), |
| | | "budgetExceeded": budget.isExhausted |
| | | ]) |
| | | return false |
| | | } |
| | | |
| | | private func finderTabButton( |
| | | matchingNormalizedTitle targetTitle: String, |
| | | in root: AXUIElement |
| | | ) -> AXUIElement? { |
| | | finderTabControl(matchingNormalizedTitle: targetTitle, in: root)?.button |
| | | } |
| | | |
| | | private func finderTabControl( |
| | | matchingNormalizedTitle targetTitle: String, |
| | | in root: AXUIElement |
| | | in root: AXUIElement, |
| | | budget: FinderTabActivationBudget |
| | | ) -> FinderTabControl? { |
| | | var visitedCount = 0 |
| | | |
| | | func search(_ element: AXUIElement, depth: Int, tabGroup: AXUIElement?) -> FinderTabControl? { |
| | | visitedCount += 1 |
| | | guard visitedCount <= 240, depth <= 9 else { return nil } |
| | | guard visitedCount <= 240, depth <= 9, !budget.isExhausted else { return nil } |
| | | |
| | | let role = axStringAttribute(kAXRoleAttribute as String, for: element) |
| | | let subrole = axStringAttribute(kAXSubroleAttribute as String, for: element) |
| | |
| | | _ host: AXWindowMetadata, |
| | | normalizedTargetTitle: String, |
| | | attempts: Int = 10, |
| | | delay: TimeInterval = 0.04 |
| | | delay: TimeInterval = 0.04, |
| | | budget: FinderTabActivationBudget? = nil |
| | | ) -> Bool { |
| | | for _ in 0..<attempts { |
| | | Thread.sleep(forTimeInterval: delay) |
| | | let attemptCount = max(1, attempts) |
| | | for attempt in 0..<attemptCount { |
| | | if attempt > 0 { |
| | | let sleepDuration = budget.map { min(delay, $0.remaining) } ?? delay |
| | | if sleepDuration > 0 { |
| | | Thread.sleep(forTimeInterval: sleepDuration) |
| | | } |
| | | } |
| | | |
| | | let selectedTitle = normalizedWindowTitle( |
| | | axStringAttribute(kAXTitleAttribute as String, for: host.element) ?? "" |
| | | ) |
| | | if selectedTitle == normalizedTargetTitle { |
| | | return true |
| | | } |
| | | |
| | | if budget?.isExhausted == true { |
| | | break |
| | | } |
| | | } |
| | | |
| | |
| | | private func selectFinderWindowMenuItem( |
| | | processIdentifier: Int32, |
| | | window: AlignerWindow, |
| | | normalizedTargetTitle: String |
| | | normalizedTargetTitle: String, |
| | | budget: FinderTabActivationBudget |
| | | ) -> Bool { |
| | | guard !budget.isExhausted else { |
| | | DevelopmentDiagnostics.log("windowActivation.finderTab.windowMenuSkippedBudgetExceeded", [ |
| | | "windowID": window.id, |
| | | "pid": processIdentifier, |
| | | "elapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit) |
| | | ]) |
| | | return false |
| | | } |
| | | |
| | | let appElement = AXUIElementCreateApplication(processIdentifier) |
| | | guard let menuBar = axElementAttribute(kAXMenuBarAttribute as String, for: appElement) else { |
| | | DevelopmentDiagnostics.log("windowActivation.finderTab.windowMenuMissing", [ |
| | |
| | | |
| | | for menuBarItem in axChildren(of: menuBar) where isWindowMenuBarItem(menuBarItem) { |
| | | let openResult = AXUIElementPerformAction(menuBarItem, kAXPressAction as CFString) |
| | | Thread.sleep(forTimeInterval: 0.08) |
| | | if budget.remaining > 0 { |
| | | Thread.sleep(forTimeInterval: min(0.08, budget.remaining)) |
| | | } |
| | | guard openResult == .success, |
| | | let menuItem = finderWindowMenuItem( |
| | | matchingNormalizedTitle: normalizedTargetTitle, |
| | | in: menuBarItem |
| | | in: menuBarItem, |
| | | budget: budget |
| | | ) |
| | | else { |
| | | continue |
| | |
| | | processIdentifier: processIdentifier, |
| | | window: window, |
| | | normalizedTargetTitle: normalizedTargetTitle, |
| | | initialDelay: 0.18, |
| | | attempts: 18, |
| | | delay: 0.06, |
| | | requiredConsecutiveMatches: 3 |
| | | initialDelay: Self.finderTabWindowMenuVerificationInitialDelay, |
| | | attempts: Self.finderTabWindowMenuVerificationAttempts, |
| | | delay: Self.finderTabWindowMenuVerificationDelay, |
| | | requiredConsecutiveMatches: 2, |
| | | budget: budget |
| | | ) { |
| | | DevelopmentDiagnostics.log("windowActivation.finderTab.windowMenuSucceeded", [ |
| | | "windowID": window.id, |
| | | "pid": processIdentifier |
| | | "pid": processIdentifier, |
| | | "elapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit) |
| | | ]) |
| | | return true |
| | | } |
| | | |
| | | DevelopmentDiagnostics.log("windowActivation.finderTab.windowMenuVerificationFailed", [ |
| | | "windowID": window.id, |
| | | "pid": processIdentifier |
| | | "pid": processIdentifier, |
| | | "elapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit), |
| | | "budgetExceeded": budget.isExhausted |
| | | ]) |
| | | } |
| | | |
| | |
| | | "windowID": window.id, |
| | | "pid": processIdentifier, |
| | | "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title), |
| | | "targetTitleLength": window.title.count |
| | | "targetTitleLength": window.title.count, |
| | | "elapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit), |
| | | "budgetExceeded": budget.isExhausted |
| | | ]) |
| | | return false |
| | | } |
| | |
| | | |
| | | private func finderWindowMenuItem( |
| | | matchingNormalizedTitle targetTitle: String, |
| | | in root: AXUIElement |
| | | in root: AXUIElement, |
| | | budget: FinderTabActivationBudget |
| | | ) -> AXUIElement? { |
| | | var visitedCount = 0 |
| | | |
| | | func search(_ element: AXUIElement, depth: Int) -> AXUIElement? { |
| | | visitedCount += 1 |
| | | guard visitedCount <= 500, depth <= 8 else { return nil } |
| | | guard visitedCount <= 500, depth <= 8, !budget.isExhausted else { return nil } |
| | | |
| | | let role = axStringAttribute(kAXRoleAttribute as String, for: element) |
| | | let title = axStringAttribute(kAXTitleAttribute as String, for: element) |
| | |
| | | return search(root, depth: 0) |
| | | } |
| | | |
| | | private func finderFocusedOrMainWindowTitle(processIdentifier: Int32) -> String? { |
| | | let appElement = AXUIElementCreateApplication(processIdentifier) |
| | | let focusedWindow = axElementAttribute(kAXFocusedWindowAttribute as String, for: appElement) |
| | | ?? axElementAttribute(kAXMainWindowAttribute as String, for: appElement) |
| | | return focusedWindow.flatMap { |
| | | axStringAttribute(kAXTitleAttribute as String, for: $0) |
| | | } |
| | | } |
| | | |
| | | private func finderFocusedWindowTitleMatches( |
| | | processIdentifier: Int32, |
| | | window: AlignerWindow, |
| | |
| | | initialDelay: TimeInterval, |
| | | attempts: Int, |
| | | delay: TimeInterval, |
| | | requiredConsecutiveMatches: Int |
| | | requiredConsecutiveMatches: Int, |
| | | budget: FinderTabActivationBudget |
| | | ) -> Bool { |
| | | let appElement = AXUIElementCreateApplication(processIdentifier) |
| | | var consecutiveMatches = 0 |
| | | var lastTitle: String? |
| | | |
| | | Thread.sleep(forTimeInterval: initialDelay) |
| | | for _ in 0..<attempts { |
| | | let focusedWindow = axElementAttribute(kAXFocusedWindowAttribute as String, for: appElement) |
| | | ?? axElementAttribute(kAXMainWindowAttribute as String, for: appElement) |
| | | let title = focusedWindow.flatMap { |
| | | axStringAttribute(kAXTitleAttribute as String, for: $0) |
| | | } |
| | | if budget.remaining > 0 { |
| | | Thread.sleep(forTimeInterval: min(initialDelay, budget.remaining)) |
| | | } |
| | | let attemptCount = max(1, attempts) |
| | | for attempt in 0..<attemptCount { |
| | | let title = finderFocusedOrMainWindowTitle(processIdentifier: processIdentifier) |
| | | lastTitle = title |
| | | if normalizedWindowTitle(title ?? "") == normalizedTargetTitle { |
| | | consecutiveMatches += 1 |
| | |
| | | if consecutiveMatches >= requiredConsecutiveMatches { |
| | | return true |
| | | } |
| | | Thread.sleep(forTimeInterval: delay) |
| | | if budget.isExhausted || attempt == attemptCount - 1 { |
| | | break |
| | | } |
| | | let sleepDuration = min(delay, budget.remaining) |
| | | if sleepDuration > 0 { |
| | | Thread.sleep(forTimeInterval: sleepDuration) |
| | | } |
| | | } |
| | | |
| | | DevelopmentDiagnostics.log("windowActivation.finderTab.focusedTitleMismatch", [ |
| | |
| | | "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title), |
| | | "targetTitleLength": window.title.count, |
| | | "lastTitleHash": DevelopmentDiagnostics.stableFingerprint(lastTitle), |
| | | "lastTitleLength": lastTitle?.count |
| | | "lastTitleLength": lastTitle?.count, |
| | | "elapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit), |
| | | "budgetExceeded": budget.isExhausted |
| | | ]) |
| | | return false |
| | | } |
| | |
| | | processIdentifier: Int32, |
| | | host: AXWindowMetadata, |
| | | window: AlignerWindow, |
| | | normalizedTargetTitle: String |
| | | normalizedTargetTitle: String, |
| | | budget: FinderTabActivationBudget |
| | | ) -> Bool { |
| | | let maximumAttempts = 24 |
| | | let maximumAttempts = Self.finderTabCycleMaximumAttempts |
| | | for attempt in 1...maximumAttempts { |
| | | guard !budget.isExhausted else { |
| | | DevelopmentDiagnostics.log("windowActivation.finderTab.cycleBudgetExceeded", [ |
| | | "windowID": window.id, |
| | | "hostWindowID": host.windowID, |
| | | "attempt": attempt, |
| | | "attemptLimit": maximumAttempts, |
| | | "elapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit) |
| | | ]) |
| | | return false |
| | | } |
| | | |
| | | for shortcut in Self.finderNextTabShortcuts { |
| | | guard postKeyboardShortcut(to: processIdentifier, shortcut: shortcut) else { |
| | | DevelopmentDiagnostics.log("windowActivation.finderTab.cyclePostFailed", [ |
| | |
| | | if finderHostTitleMatchesTarget( |
| | | host, |
| | | normalizedTargetTitle: normalizedTargetTitle, |
| | | attempts: 4, |
| | | delay: 0.035 |
| | | attempts: 2, |
| | | delay: 0.025, |
| | | budget: budget |
| | | ) { |
| | | DevelopmentDiagnostics.log("windowActivation.finderTab.cycleSucceeded", [ |
| | | "windowID": window.id, |
| | | "hostWindowID": host.windowID, |
| | | "attempt": attempt, |
| | | "shortcut": shortcut.name |
| | | "attemptLimit": maximumAttempts, |
| | | "shortcut": shortcut.name, |
| | | "elapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit) |
| | | ]) |
| | | return true |
| | | } |
| | |
| | | DevelopmentDiagnostics.log("windowActivation.finderTab.cycleFailed", [ |
| | | "windowID": window.id, |
| | | "hostWindowID": host.windowID, |
| | | "attemptCount": maximumAttempts |
| | | "attemptCount": maximumAttempts, |
| | | "elapsedMilliseconds": budget.elapsedMilliseconds, |
| | | "budgetMilliseconds": Self.milliseconds(budget.limit), |
| | | "budgetExceeded": budget.isExhausted |
| | | ]) |
| | | return false |
| | | } |
| | |
| | | } |
| | | |
| | | let spaceIDs = spaceIDsByWindowID[resolvedWindowID] ?? [] |
| | | nextRestorableAXWindowsByID[resolvedWindowID] = RestorableAXWindow( |
| | | processIdentifier: axWindow.processIdentifier, |
| | | element: axWindow.element |
| | | ) |
| | | let isFullscreen = !Set(spaceIDs).isDisjoint(with: fullscreenSpaceIDs) |
| | | guard WindowEnumerationPolicy.shouldIncludeAXOnlyRecord( |
| | | identifierSource: identifierSource, |
| | | isMinimized: axWindow.isMinimized, |
| | | isFullscreen: isFullscreen, |
| | | spaceIDs: spaceIDs |
| | | ) else { |
| | | DevelopmentDiagnostics.log("windowEnumeration.axOnly.lowConfidenceSkipped", [ |
| | | "windowID": resolvedWindowID, |
| | | "pid": axWindow.processIdentifier, |
| | | "appName": axWindow.app.name, |
| | | "bundleIdentifier": axWindow.app.bundleIdentifier, |
| | | "titleHash": DevelopmentDiagnostics.stableFingerprint(axWindow.title ?? ""), |
| | | "titleLength": axWindow.title?.count ?? 0, |
| | | "identifierSource": String(describing: identifierSource), |
| | | "isMinimized": axWindow.isMinimized, |
| | | "isFullscreen": isFullscreen, |
| | | "spaceIDCount": spaceIDs.count |
| | | ]) |
| | | continue |
| | | } |
| | | if axWindow.isMinimized { |
| | | nextRestorableAXWindowsByID[resolvedWindowID] = RestorableAXWindow( |
| | | processIdentifier: axWindow.processIdentifier, |
| | | element: axWindow.element |
| | | ) |
| | | } |
| | | axOnlyRecords.append(WindowEnumerationRecord( |
| | | id: resolvedWindowID, |
| | | app: axWindow.app, |
| | |
| | | frame: axWindow.frame, |
| | | subrole: axWindow.subrole, |
| | | isMinimized: axWindow.isMinimized, |
| | | isFullscreen: !Set(spaceIDs).isDisjoint(with: fullscreenSpaceIDs), |
| | | isFullscreen: isFullscreen, |
| | | isGhost: false, |
| | | isDesktopElement: false, |
| | | isSystemCritical: isSystemCritical(app: axWindow.app), |
| | |
| | | return activated |
| | | } |
| | | |
| | | private func retryFocusMainRaiseWindow( |
| | | for window: AlignerWindow, |
| | | delay: TimeInterval, |
| | | matchAttempt: String |
| | | ) -> WindowFocusAttempt? { |
| | | if delay > 0 { |
| | | Thread.sleep(forTimeInterval: delay) |
| | | } |
| | | |
| | | let retryMetadata = AXWindowMetadataReader.metadata(appCategorizer: appCategorizer) |
| | | guard let retryAXWindow = axWindow( |
| | | for: window, |
| | | in: retryMetadata, |
| | | operation: "windowActivation", |
| | | matchAttempt: matchAttempt |
| | | ) else { |
| | | DevelopmentDiagnostics.log("windowActivation.activate.retryAXLookupMissing", [ |
| | | "windowID": window.id, |
| | | "pid": window.app.processIdentifier, |
| | | "attempt": matchAttempt, |
| | | "candidateCount": retryMetadata.filter { $0.processIdentifier == window.app.processIdentifier }.count, |
| | | "matchingTitleCandidateCount": matchingAXTitleCandidateCount(for: window, in: retryMetadata), |
| | | "matchingSizeCandidateCount": matchingAXSizeCandidateCount(for: window, in: retryMetadata) |
| | | ]) |
| | | return nil |
| | | } |
| | | |
| | | let attempt = focusMainRaiseWindowResult( |
| | | retryAXWindow.element, |
| | | processIdentifier: retryAXWindow.processIdentifier |
| | | ) |
| | | DevelopmentDiagnostics.log("windowActivation.activate.retryAXFocus", [ |
| | | "windowID": window.id, |
| | | "pid": retryAXWindow.processIdentifier, |
| | | "attempt": matchAttempt, |
| | | "matchedWindowID": retryAXWindow.windowID, |
| | | "matchedTitleHash": DevelopmentDiagnostics.stableFingerprint(retryAXWindow.title), |
| | | "focused": attempt.focused.success, |
| | | "focusAXError": String(describing: attempt.focused.error), |
| | | "main": attempt.main.success, |
| | | "mainAXError": String(describing: attempt.main.error), |
| | | "raised": attempt.raised.success, |
| | | "raiseAXError": String(describing: attempt.raised.error) |
| | | ]) |
| | | return attempt |
| | | } |
| | | |
| | | private func focusMainRaiseWindowResult( |
| | | _ window: AXUIElement, |
| | | processIdentifier: Int32 |
| | | ) -> WindowFocusAttempt { |
| | | WindowFocusAttempt( |
| | | focused: focusWindowResult(window, processIdentifier: processIdentifier), |
| | | main: mainWindowResult(window), |
| | | raised: raiseWindowResult(window) |
| | | ) |
| | | } |
| | | |
| | | private func focusWindowResult(_ window: AXUIElement, processIdentifier: Int32) -> AXActionResult { |
| | | let appElement = AXUIElementCreateApplication(processIdentifier) |
| | | let error = AXUIElementSetAttributeValue( |
| | |
| | | return AXActionResult(error: error) |
| | | } |
| | | |
| | | private func mainWindowResult(_ window: AXUIElement) -> AXActionResult { |
| | | let error = AXUIElementSetAttributeValue( |
| | | window, |
| | | kAXMainAttribute as CFString, |
| | | kCFBooleanTrue |
| | | ) |
| | | return AXActionResult(error: error) |
| | | } |
| | | |
| | | private func raiseWindowResult(_ window: AXUIElement) -> AXActionResult { |
| | | AXActionResult(error: AXUIElementPerformAction(window, kAXRaiseAction as CFString)) |
| | | } |
| | | |
| | | private static func elapsedMilliseconds(since startedAt: TimeInterval) -> Int { |
| | | milliseconds(ProcessInfo.processInfo.systemUptime - startedAt) |
| | | } |
| | | |
| | | private static func milliseconds(_ interval: TimeInterval) -> Int { |
| | | Int((max(0, interval) * 1_000).rounded()) |
| | | } |
| | | |
| | | private func activationPolicy(from runningApplication: NSRunningApplication?) -> AlignerActivationPolicy { |
| | | guard let runningApplication else { return .unknown } |
| | | |