From 21e9dd7c95e3f58a19a7c7342ff3d6584eb9097e Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 14 Aug 2026 04:45:56 +0800
Subject: [PATCH] fix: reserve app shelf height for wrapped rows
---
C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift | 731 ++++++++++++++++++++++++++++++++++++++++++--------------
1 files changed, 549 insertions(+), 182 deletions(-)
diff --git a/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift b/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
index 21abec8..15fbdda 100644
--- a/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
+++ b/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
@@ -25,6 +25,51 @@
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(),
@@ -82,51 +127,49 @@
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: AlignerWindow) throws -> WindowActivationResult {
@@ -215,68 +258,129 @@
}
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: AlignerWindow) throws -> WindowCloseResult {
@@ -620,14 +724,41 @@
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
+ normalizedTargetTitle: normalizedTargetTitle,
+ budget: budget
)
DevelopmentDiagnostics.log("\(operation).finderTab.hostlessWindowMenuResult", [
"windowID": window.id,
@@ -637,7 +768,9 @@
"selectedViaWindowMenu": selectedViaWindowMenu,
"spaceIDs": window.spaceIDs,
"targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
- "targetTitleLength": window.title.count
+ "targetTitleLength": window.title.count,
+ "elapsedMilliseconds": budget.elapsedMilliseconds,
+ "budgetMilliseconds": Self.milliseconds(budget.limit)
])
if selectedViaWindowMenu {
return true
@@ -647,7 +780,9 @@
"windowID": window.id,
"pid": processIdentifier,
"reason": reason,
- "spaceIDs": window.spaceIDs
+ "spaceIDs": window.spaceIDs,
+ "elapsedMilliseconds": budget.elapsedMilliseconds,
+ "budgetMilliseconds": Self.milliseconds(budget.limit)
])
return false
}
@@ -664,26 +799,32 @@
}
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)
@@ -697,11 +838,16 @@
"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
@@ -715,7 +861,8 @@
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
@@ -735,16 +882,9 @@
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 = 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 {
@@ -760,39 +900,66 @@
return (lhs.metadata.windowID ?? UInt32.max) < (rhs.metadata.windowID ?? UInt32.max)
}
- if let best = scoredHosts.first {
- if scoredHosts.count == 1 {
- if selectableHostCandidates.isEmpty {
- DevelopmentDiagnostics.log("windowActivation.finderTab.frameInferredHost", [
- "windowID": window.id,
- "hostWindowID": best.metadata.windowID,
- "score": Double(best.score),
- "spaceIDCount": targetSpaceIDs.count,
- "candidateCount": hostCandidates.count,
- "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
- "targetTitleLength": window.title.count
- ])
- }
+ 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 {
- if selectableHostCandidates.isEmpty {
- DevelopmentDiagnostics.log("windowActivation.finderTab.frameInferredHost", [
- "windowID": window.id,
- "hostWindowID": best.metadata.windowID,
- "score": Double(best.score),
- "secondBestScore": Double(secondBest.score),
- "spaceIDCount": targetSpaceIDs.count,
- "candidateCount": hostCandidates.count,
- "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
- "targetTitleLength": window.title.count
- ])
- }
+ 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 {
@@ -820,7 +987,8 @@
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 }
@@ -829,12 +997,14 @@
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 }
@@ -845,28 +1015,35 @@
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
}
@@ -885,7 +1062,13 @@
"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
}
@@ -901,7 +1084,13 @@
"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
}
}
@@ -917,25 +1106,35 @@
"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
}
@@ -947,27 +1146,24 @@
"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)
@@ -1001,15 +1197,27 @@
_ 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
}
}
@@ -1019,8 +1227,19 @@
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", [
@@ -1032,11 +1251,14 @@
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
@@ -1054,21 +1276,27 @@
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
])
}
@@ -1076,7 +1304,10 @@
"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
}
@@ -1088,13 +1319,14 @@
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)
@@ -1116,6 +1348,15 @@
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,
@@ -1123,19 +1364,18 @@
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
@@ -1146,7 +1386,13 @@
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", [
@@ -1155,7 +1401,10 @@
"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
}
@@ -1164,10 +1413,23 @@
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", [
@@ -1182,14 +1444,18 @@
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
}
@@ -1199,7 +1465,10 @@
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
}
@@ -1601,10 +1870,33 @@
}
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,
@@ -1615,7 +1907,7 @@
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),
@@ -1713,6 +2005,64 @@
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(
@@ -1723,10 +2073,27 @@
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 }
--
Gitblit v1.9.3