From bedb145ee1413482763707fe73787b622cb52cbf Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sat, 13 Jun 2026 16:08:40 +0800
Subject: [PATCH] Fix Finder tab space activation
---
C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift | 342 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 331 insertions(+), 11 deletions(-)
diff --git a/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift b/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
index 87b6c01..b1b1cd3 100644
--- a/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
+++ b/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
@@ -162,9 +162,10 @@
let privateActivationOutcome = activateViaPrivateWindowServerAPI(window)
let didPrivatelyActivate = privateActivationOutcome?.succeeded == true
+ let currentAXMetadata = AXWindowMetadataReader.metadata(appCategorizer: appCategorizer)
let axWindow = axWindow(
for: window,
- in: AXWindowMetadataReader.metadata(appCategorizer: appCategorizer),
+ in: currentAXMetadata,
operation: "windowActivation",
matchAttempt: "initial"
)
@@ -188,6 +189,20 @@
"spaceFocusError": spaceFocusOutcome?.error
])
guard let axWindow else {
+ if let finderTabActivated = activateFinderTabPageIfNeeded(
+ window: window,
+ axMetadata: currentAXMetadata,
+ operation: "windowActivation",
+ reason: "axMissing"
+ ) {
+ DevelopmentDiagnostics.log("windowActivation.activate.finderTabResult", [
+ "windowID": window.id,
+ "pid": window.app.processIdentifier,
+ "activated": finderTabActivated
+ ])
+ return finderTabActivated ? .activated : .activationFailed
+ }
+
let activated = activateApplication(for: window)
DevelopmentDiagnostics.log("windowActivation.activate.axMissing", [
"windowID": window.id,
@@ -393,15 +408,31 @@
return nil
}
- guard let activationOutcome = activateViaPrivateWindowServerAPI(window),
- activationOutcome.succeeded
- else {
- DevelopmentDiagnostics.log("windowClose.commandW.activationFailed", [
- "windowID": window.id,
- "pid": processIdentifier,
- "reason": reason
- ])
- return nil
+ if let finderTabActivated = activateFinderTabPageIfNeeded(
+ window: window,
+ axMetadata: AXWindowMetadataReader.metadata(appCategorizer: appCategorizer),
+ operation: "windowClose",
+ reason: reason
+ ) {
+ guard finderTabActivated else {
+ DevelopmentDiagnostics.log("windowClose.commandW.finderTabActivationFailed", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "reason": reason
+ ])
+ return .failed("finderTabActivationFailed")
+ }
+ } else {
+ guard let activationOutcome = activateViaPrivateWindowServerAPI(window),
+ activationOutcome.succeeded
+ else {
+ DevelopmentDiagnostics.log("windowClose.commandW.activationFailed", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "reason": reason
+ ])
+ return nil
+ }
}
Thread.sleep(forTimeInterval: 0.08)
@@ -564,6 +595,271 @@
&& metadata.subrole == .standard
}
return finderCandidates.count == 1 ? finderCandidates[0] : nil
+ }
+
+ private func activateFinderTabPageIfNeeded(
+ window: AlignerWindow,
+ axMetadata: [AXWindowMetadata],
+ operation: String,
+ reason: String
+ ) -> Bool? {
+ guard isFinderCGOnlyPageCandidate(window),
+ let processIdentifier = window.app.processIdentifier
+ else {
+ return nil
+ }
+
+ guard let host = finderTabHostAXWindow(for: window, in: axMetadata) else {
+ DevelopmentDiagnostics.log("\(operation).finderTab.hostMissing", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "reason": reason,
+ "spaceIDs": window.spaceIDs
+ ])
+ return false
+ }
+
+ let hostPrivateActivationSucceeded: Bool
+ if let hostWindowID = host.windowID,
+ let privateActivationBridge {
+ hostPrivateActivationSucceeded = privateActivationBridge.activate(
+ processIdentifier: processIdentifier,
+ windowID: hostWindowID
+ ).succeeded
+ } else {
+ hostPrivateActivationSucceeded = 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 = focusWindowResult(host.element, processIdentifier: processIdentifier)
+ let raiseAfterResult = raiseWindowResult(host.element)
+
+ let didFocusOrRaise = focusBeforeResult.success
+ || raiseBeforeResult.success
+ || focusAfterResult.success
+ || raiseAfterResult.success
+ let activated = selectedTab
+ && (hostPrivateActivationSucceeded || applicationActivated || didFocusOrRaise)
+
+ DevelopmentDiagnostics.log("\(operation).finderTab.result", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "reason": reason,
+ "hostWindowID": host.windowID,
+ "hostTitleHash": DevelopmentDiagnostics.stableFingerprint(host.title),
+ "hostTitleLength": host.title?.count,
+ "hostPrivateActivated": hostPrivateActivationSucceeded,
+ "applicationActivated": applicationActivated,
+ "selectedTab": selectedTab,
+ "focusBeforeAXError": String(describing: focusBeforeResult.error),
+ "raiseBeforeAXError": String(describing: raiseBeforeResult.error),
+ "focusAfterAXError": String(describing: focusAfterResult.error),
+ "raiseAfterAXError": String(describing: raiseAfterResult.error),
+ "activated": activated
+ ])
+
+ return activated
+ }
+
+ private func isFinderCGOnlyPageCandidate(_ window: AlignerWindow) -> Bool {
+ window.identifierSource == .cgWindow
+ && window.app.bundleIdentifier == "com.apple.finder"
+ && !window.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+ }
+
+ private func finderTabHostAXWindow(
+ for window: AlignerWindow,
+ in axMetadata: [AXWindowMetadata]
+ ) -> AXWindowMetadata? {
+ guard let processIdentifier = window.app.processIdentifier else { return nil }
+ let candidates = axMetadata.filter { metadata in
+ metadata.processIdentifier == processIdentifier
+ && metadata.subrole == .standard
+ && metadata.frame?.isEmpty == false
+ }
+ guard !candidates.isEmpty else { return nil }
+
+ let targetSpaceIDs = Set(window.spaceIDs)
+ let candidateIDs = candidates.compactMap(\.windowID)
+ let candidateSpacesByID = targetSpaceIDs.isEmpty ? [:] : spaceIDsByWindowIDProvider(candidateIDs)
+ let spaceCompatibleCandidates = targetSpaceIDs.isEmpty
+ ? candidates
+ : candidates.filter { metadata in
+ guard let windowID = metadata.windowID else { return false }
+ return !Set(candidateSpacesByID[windowID] ?? []).isDisjoint(with: targetSpaceIDs)
+ }
+ let hostCandidates = spaceCompatibleCandidates.isEmpty ? candidates : spaceCompatibleCandidates
+ let targetFrame = cgWindowFrame(for: window) ?? window.frame
+ let scoredHosts = hostCandidates.compactMap { metadata -> (metadata: AXWindowMetadata, score: CGFloat)? in
+ guard let score = frameOverlapScore(targetFrame, metadata.frame),
+ score >= 0.85
+ else {
+ return nil
+ }
+
+ return (metadata, score)
+ }
+ .sorted { lhs, rhs in
+ if lhs.score != rhs.score {
+ return lhs.score > rhs.score
+ }
+ return (lhs.metadata.windowID ?? UInt32.max) < (rhs.metadata.windowID ?? UInt32.max)
+ }
+
+ if let best = scoredHosts.first {
+ if scoredHosts.count == 1 {
+ return best.metadata
+ }
+
+ let secondBest = scoredHosts[1]
+ if best.score - secondBest.score >= 0.05 {
+ return best.metadata
+ }
+ }
+
+ return hostCandidates.count == 1 ? hostCandidates[0] : nil
+ }
+
+ private func selectFinderTab(
+ _ window: AlignerWindow,
+ in host: AXWindowMetadata
+ ) -> Bool {
+ let normalizedTargetTitle = normalizedWindowTitle(window.title)
+ guard !normalizedTargetTitle.isEmpty else { return false }
+
+ if normalizedWindowTitle(host.title ?? "") == normalizedTargetTitle {
+ return true
+ }
+
+ guard let tabButton = finderTabButton(
+ matchingNormalizedTitle: normalizedTargetTitle,
+ in: host.element
+ ) else {
+ DevelopmentDiagnostics.log("windowActivation.finderTab.tabButtonMissing", [
+ "windowID": window.id,
+ "hostWindowID": host.windowID,
+ "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
+ "targetTitleLength": window.title.count
+ ])
+ return false
+ }
+
+ let tabButtonRole = axStringAttribute(kAXRoleAttribute as String, for: tabButton)
+ let tabButtonSubrole = axStringAttribute(kAXSubroleAttribute as String, for: tabButton)
+ let pressResult = AXUIElementPerformAction(tabButton, kAXPressAction as CFString)
+ DevelopmentDiagnostics.log("windowActivation.finderTab.pressTab", [
+ "windowID": window.id,
+ "hostWindowID": host.windowID,
+ "tabButtonRole": tabButtonRole,
+ "tabButtonSubrole": tabButtonSubrole,
+ "axError": String(describing: pressResult)
+ ])
+ guard pressResult == .success else { return false }
+
+ for _ in 0..<10 {
+ Thread.sleep(forTimeInterval: 0.04)
+ let selectedTitle = normalizedWindowTitle(
+ axStringAttribute(kAXTitleAttribute as String, for: host.element) ?? ""
+ )
+ if selectedTitle == normalizedTargetTitle {
+ return true
+ }
+ }
+
+ let currentHostTitle = axStringAttribute(kAXTitleAttribute as String, for: host.element) ?? ""
+ DevelopmentDiagnostics.log("windowActivation.finderTab.selectionVerificationFailed", [
+ "windowID": window.id,
+ "hostWindowID": host.windowID,
+ "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
+ "targetTitleLength": window.title.count,
+ "currentHostTitleHash": DevelopmentDiagnostics.stableFingerprint(currentHostTitle),
+ "currentHostTitleLength": currentHostTitle.count
+ ])
+ return false
+ }
+
+ private func finderTabButton(
+ matchingNormalizedTitle targetTitle: String,
+ in root: AXUIElement
+ ) -> AXUIElement? {
+ var visitedCount = 0
+
+ func search(_ element: AXUIElement, depth: Int, insideTabGroup: Bool) -> AXUIElement? {
+ visitedCount += 1
+ guard visitedCount <= 240, depth <= 9 else { return nil }
+
+ let role = axStringAttribute(kAXRoleAttribute as String, for: element)
+ let subrole = axStringAttribute(kAXSubroleAttribute as String, for: element)
+ let title = axStringAttribute(kAXTitleAttribute as String, for: element)
+ ?? axStringAttribute(kAXDescriptionAttribute as String, for: element)
+ ?? axStringAttribute(kAXValueAttribute as String, for: element)
+ let isTabGroup = role == "AXTabGroup"
+ let isTabControl = subrole == "AXTabButton"
+ || (insideTabGroup && (role == kAXRadioButtonRole as String || role == kAXButtonRole as String))
+
+ if normalizedWindowTitle(title ?? "") == targetTitle,
+ isTabControl {
+ return element
+ }
+
+ guard isTabGroup || insideTabGroup || depth < 7 else { return nil }
+ for child in axChildren(of: element) {
+ if let match = search(child, depth: depth + 1, insideTabGroup: insideTabGroup || isTabGroup) {
+ return match
+ }
+ }
+
+ return nil
+ }
+
+ return search(root, depth: 0, insideTabGroup: false)
+ }
+
+ private func axChildren(of element: AXUIElement) -> [AXUIElement] {
+ var value: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(element, kAXChildrenAttribute as CFString, &value) == .success,
+ let children = value as? [AXUIElement]
+ else {
+ return []
+ }
+
+ return children
+ }
+
+ private func axStringAttribute(_ attribute: String, for element: AXUIElement) -> String? {
+ var value: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
+ let value
+ else {
+ return nil
+ }
+
+ return value as? String
+ }
+
+ private func frameOverlapScore(_ lhs: CGRect?, _ rhs: CGRect?) -> CGFloat? {
+ guard let lhs,
+ let rhs,
+ !lhs.isEmpty,
+ !rhs.isEmpty
+ else {
+ return nil
+ }
+
+ let intersection = lhs.intersection(rhs)
+ guard !intersection.isNull,
+ !intersection.isEmpty
+ else {
+ return nil
+ }
+
+ let denominator = min(lhs.width * lhs.height, rhs.width * rhs.height)
+ guard denominator > 0 else { return nil }
+ return (intersection.width * intersection.height) / denominator
}
private func cgWindowFingerprint(for window: AlignerWindow) -> AXWindowFingerprint? {
@@ -895,7 +1191,31 @@
}
restorableAXWindowsByID = nextRestorableAXWindowsByID
- return cgRecords + axOnlyRecords
+ let records = cgRecords + axOnlyRecords
+ let attributedRecords = FinderTabSpaceAttributionPolicy.attributedRecords(records)
+ DevelopmentDiagnostics.log("windowEnumeration.finderTabSpaceAttribution", [
+ "finderNoSpaceCandidateCount": records.filter { record in
+ record.app.bundleIdentifier == "com.apple.finder"
+ && record.identifierSource == .cgWindow
+ && record.spaceIDs.isEmpty
+ && !record.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+ && record.frame?.isEmpty == false
+ }.count,
+ "finderFullscreenHostCount": records.filter { record in
+ record.app.bundleIdentifier == "com.apple.finder"
+ && record.isFullscreen
+ && !record.spaceIDs.isEmpty
+ && record.subrole == .standard
+ && record.frame?.isEmpty == false
+ }.count,
+ "attributedCount": zip(records, attributedRecords).filter { before, after in
+ before.spaceIDs.isEmpty && !after.spaceIDs.isEmpty
+ }.count,
+ "attributedWindowIDs": zip(records, attributedRecords).compactMap { before, after in
+ before.spaceIDs.isEmpty && !after.spaceIDs.isEmpty ? after.id : nil
+ }
+ ])
+ return attributedRecords
}
private func app(
--
Gitblit v1.9.3