From 343237725f65445bab306e3fe4cdbd10949d4d4b Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sat, 13 Jun 2026 18:47:36 +0800
Subject: [PATCH] Fix Finder tab page activation
---
C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift | 1398 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 1,374 insertions(+), 24 deletions(-)
diff --git a/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift b/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
index 773abed..e03dddb 100644
--- a/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
+++ b/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
@@ -1,10 +1,10 @@
import AppKit
-import ApplicationServices
+@preconcurrency import ApplicationServices
import CoreGraphics
import Foundation
import AlignerCore
-final class CGWindowAXWindowService: WindowServiceProtocol, WindowRestorationServiceProtocol, WindowActivationServiceProtocol {
+final class CGWindowAXWindowService: WindowServiceProtocol, WindowRestorationServiceProtocol, WindowActivationServiceProtocol, WindowCloseServiceProtocol {
weak var delegate: WindowServiceDelegate?
private let appCategorizer: any AppCategorizer
@@ -13,6 +13,17 @@
private let privateActivationBridge: PrivateWindowActivationBridge?
private let privateSpaceActivationBridge: PrivateSpaceActivationBridge?
private var restorableAXWindowsByID: [UInt32: RestorableAXWindow] = [:]
+
+ private struct FinderTabControl {
+ let button: AXUIElement
+ let tabGroup: AXUIElement?
+ }
+
+ private struct KeyboardShortcut {
+ let name: String
+ let keyCode: CGKeyCode
+ let flags: CGEventFlags
+ }
init(
spaceIDsByWindowIDProvider: @escaping ([UInt32]) -> [UInt32: [UInt64]],
@@ -118,7 +129,8 @@
: .activationFailed
}
- func activate(window: AlignerWindow) throws -> WindowActivationResult {
+ func activate(window requestedWindow: AlignerWindow) throws -> WindowActivationResult {
+ let window = refreshedFinderWindowForPreciseAction(requestedWindow, operation: "windowActivation")
DevelopmentDiagnostics.log("windowActivation.activate.start", [
"windowID": window.id,
"identifierSource": String(describing: window.identifierSource),
@@ -162,16 +174,13 @@
let privateActivationOutcome = activateViaPrivateWindowServerAPI(window)
let didPrivatelyActivate = privateActivationOutcome?.succeeded == true
- let axWindow = AXWindowMetadataReader
- .metadata(appCategorizer: appCategorizer)
- .first { metadata in
- if let windowID = metadata.windowID {
- return windowID == window.id
- }
-
- return metadata.processIdentifier == window.app.processIdentifier
- && metadata.title == window.title
- }
+ let currentAXMetadata = AXWindowMetadataReader.metadata(appCategorizer: appCategorizer)
+ let axWindow = axWindow(
+ for: window,
+ in: currentAXMetadata,
+ operation: "windowActivation",
+ matchAttempt: "initial"
+ )
DevelopmentDiagnostics.log("windowActivation.activate.axLookup", [
"windowID": window.id,
@@ -192,6 +201,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,
@@ -255,6 +278,1138 @@
return didPrivatelyActivate || focusedBeforeActivate || focusedAfterActivate || raisedBeforeActivate || raisedAfterActivate
? .activated
: .appActivatedOnly
+ }
+
+ func close(window requestedWindow: AlignerWindow) throws -> WindowCloseResult {
+ let window = refreshedFinderWindowForPreciseAction(requestedWindow, operation: "windowClose")
+ DevelopmentDiagnostics.log("windowClose.window.start", [
+ "windowID": window.id,
+ "identifierSource": String(describing: window.identifierSource),
+ "titleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
+ "titleLength": window.title.count,
+ "titleIsEmpty": window.title.isEmpty,
+ "appName": window.app.name,
+ "bundleIdentifier": window.app.bundleIdentifier,
+ "pid": window.app.processIdentifier
+ ])
+
+ let axMetadata = AXWindowMetadataReader.metadata(appCategorizer: appCategorizer)
+ var matchedAXWindow = axWindow(for: window, in: axMetadata)
+ var finalAXMetadata = axMetadata
+
+ let shouldPreferFocusedPageClose = matchedAXWindow?.windowID != window.id
+ if shouldPreferFocusedPageClose,
+ let focusedPageCloseResult = closeFocusedPageViaCommandW(
+ window: window,
+ reason: matchedAXWindow == nil ? "axMissing" : "nonDirectAXMatch"
+ ) {
+ return focusedPageCloseResult
+ }
+
+ if matchedAXWindow == nil {
+ Thread.sleep(forTimeInterval: 0.12)
+ finalAXMetadata = AXWindowMetadataReader.metadata(appCategorizer: appCategorizer)
+ matchedAXWindow = axWindow(for: window, in: finalAXMetadata, matchAttempt: "retry")
+ }
+
+ guard let matchedAXWindow else {
+ DevelopmentDiagnostics.log("windowClose.window.axMissing", [
+ "windowID": window.id,
+ "appName": window.app.name,
+ "pid": window.app.processIdentifier,
+ "candidateCount": finalAXMetadata.filter { $0.processIdentifier == window.app.processIdentifier }.count,
+ "matchingTitleCandidateCount": matchingAXTitleCandidateCount(for: window, in: finalAXMetadata),
+ "matchingSizeCandidateCount": matchingAXSizeCandidateCount(for: window, in: finalAXMetadata)
+ ])
+ return .windowNotFound
+ }
+
+ if matchedAXWindow.windowID != window.id,
+ let focusedPageCloseResult = closeFocusedPageViaCommandW(
+ window: window,
+ reason: "retryNonDirectAXMatch"
+ ) {
+ return focusedPageCloseResult
+ }
+
+ guard let closeButton = closeButton(for: matchedAXWindow) else {
+ Thread.sleep(forTimeInterval: 0.12)
+ let retryMetadata = AXWindowMetadataReader.metadata(appCategorizer: appCategorizer)
+ if let retryAXWindow = axWindow(for: window, in: retryMetadata, matchAttempt: "closeButtonRetry"),
+ let retryCloseButton = closeButton(for: retryAXWindow) {
+ return performCloseButtonPress(
+ retryCloseButton,
+ window: window,
+ processIdentifier: retryAXWindow.processIdentifier,
+ attempt: "closeButtonRetry"
+ )
+ }
+ DevelopmentDiagnostics.log("windowClose.window.closeButtonMissing", [
+ "windowID": window.id,
+ "pid": matchedAXWindow.processIdentifier,
+ "appName": window.app.name
+ ])
+ return .windowNotFound
+ }
+
+ return performCloseButtonPress(
+ closeButton,
+ window: window,
+ 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? {
+ var closeButtonValue: CFTypeRef?
+ let closeButtonLookupResult = AXUIElementCopyAttributeValue(
+ axWindow.element,
+ kAXCloseButtonAttribute as CFString,
+ &closeButtonValue
+ )
+ guard closeButtonLookupResult == .success,
+ let closeButton = closeButtonValue,
+ CFGetTypeID(closeButton) == AXUIElementGetTypeID()
+ else {
+ DevelopmentDiagnostics.log("windowClose.window.closeButtonLookupFailed", [
+ "pid": axWindow.processIdentifier,
+ "axError": String(describing: closeButtonLookupResult)
+ ])
+ return nil
+ }
+
+ return (closeButton as! AXUIElement)
+ }
+
+ private func performCloseButtonPress(
+ _ closeButton: AXUIElement,
+ window: AlignerWindow,
+ processIdentifier: Int32,
+ attempt: String
+ ) -> WindowCloseResult {
+ let closeResult = AXUIElementPerformAction(closeButton, kAXPressAction as CFString)
+ DevelopmentDiagnostics.log("windowClose.window.result", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "attempt": attempt,
+ "axError": String(describing: closeResult)
+ ])
+
+ switch closeResult {
+ case .success:
+ return .requested
+ case .actionUnsupported, .notImplemented:
+ return .unsupported
+ default:
+ return .failed(String(describing: closeResult))
+ }
+ }
+
+ private func closeFocusedPageViaCommandW(
+ window: AlignerWindow,
+ reason: String
+ ) -> WindowCloseResult? {
+ guard window.identifierSource == .cgWindow,
+ let processIdentifier = window.app.processIdentifier
+ else {
+ DevelopmentDiagnostics.log("windowClose.commandW.skipped", [
+ "windowID": window.id,
+ "reason": reason,
+ "identifierSource": String(describing: window.identifierSource),
+ "hasPID": window.app.processIdentifier != nil
+ ])
+ 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)
+ guard postCommandW(to: processIdentifier) else {
+ DevelopmentDiagnostics.log("windowClose.commandW.postFailed", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "reason": reason
+ ])
+ return .failed("commandWPostFailed")
+ }
+
+ DevelopmentDiagnostics.log("windowClose.commandW.requested", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "reason": reason
+ ])
+ return .requested
+ }
+
+ private func postCommandW(to processIdentifier: Int32) -> Bool {
+ let source = CGEventSource(stateID: .combinedSessionState)
+ guard let keyDown = CGEvent(
+ keyboardEventSource: source,
+ virtualKey: Self.commandWVirtualKeyCode,
+ keyDown: true
+ ),
+ let keyUp = CGEvent(
+ keyboardEventSource: source,
+ virtualKey: Self.commandWVirtualKeyCode,
+ keyDown: false
+ )
+ else {
+ return false
+ }
+
+ keyDown.flags = .maskCommand
+ keyUp.flags = .maskCommand
+ keyDown.postToPid(processIdentifier)
+ keyUp.postToPid(processIdentifier)
+ return true
+ }
+
+ private func axWindow(
+ for window: AlignerWindow,
+ in axMetadata: [AXWindowMetadata],
+ operation: String = "windowClose",
+ matchAttempt: String = "initial"
+ ) -> AXWindowMetadata? {
+ if let directMatch = axMetadata.first(where: { $0.windowID == window.id }) {
+ DevelopmentDiagnostics.log("\(operation).axMatch.directWindowID", [
+ "windowID": window.id,
+ "pid": window.app.processIdentifier,
+ "attempt": matchAttempt
+ ])
+ return directMatch
+ }
+
+ guard let processIdentifier = window.app.processIdentifier else {
+ return nil
+ }
+
+ if let geometryFingerprint = axWindowGeometryFingerprint(for: window) {
+ let geometryMatches = axMetadata.filter { metadata in
+ AXWindowGeometryFingerprint(metadata) == geometryFingerprint
+ }
+ if geometryMatches.count == 1 {
+ DevelopmentDiagnostics.log("\(operation).axMatch.geometryFingerprint", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "attempt": matchAttempt
+ ])
+ return geometryMatches[0]
+ }
+
+ if geometryMatches.count > 1 {
+ DevelopmentDiagnostics.log("\(operation).axMatch.geometryFingerprintAmbiguous", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "candidateCount": geometryMatches.count,
+ "attempt": matchAttempt
+ ])
+ }
+ }
+
+ if let cgFingerprint = cgWindowFingerprint(for: window) {
+ let fingerprintMatches = axMetadata.filter { metadata in
+ AXWindowFingerprint(metadata) == cgFingerprint
+ }
+ if fingerprintMatches.count == 1 {
+ DevelopmentDiagnostics.log("\(operation).axMatch.fingerprint", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "attempt": matchAttempt
+ ])
+ return fingerprintMatches[0]
+ }
+
+ if fingerprintMatches.count > 1 {
+ DevelopmentDiagnostics.log("\(operation).axMatch.fingerprintAmbiguous", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "candidateCount": fingerprintMatches.count,
+ "attempt": matchAttempt
+ ])
+ }
+ }
+
+ let normalizedTitle = normalizedWindowTitle(window.title)
+ let titleMatches = axMetadata.filter { metadata in
+ metadata.processIdentifier == processIdentifier
+ && normalizedWindowTitle(metadata.title ?? "") == normalizedTitle
+ }
+ if !normalizedTitle.isEmpty, titleMatches.count == 1 {
+ DevelopmentDiagnostics.log("\(operation).axMatch.normalizedTitle", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "attempt": matchAttempt
+ ])
+ return titleMatches[0]
+ }
+
+ if window.app.bundleIdentifier == "com.apple.finder",
+ let finderMatch = finderFallbackAXWindow(
+ for: window,
+ processIdentifier: processIdentifier,
+ in: axMetadata
+ ) {
+ DevelopmentDiagnostics.log("\(operation).axMatch.finderFallback", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "attempt": matchAttempt
+ ])
+ return finderMatch
+ }
+
+ if titleMatches.count > 1 {
+ DevelopmentDiagnostics.log("\(operation).axMatch.titleAmbiguous", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "candidateCount": titleMatches.count,
+ "attempt": matchAttempt
+ ])
+ }
+
+ return nil
+ }
+
+ private func finderFallbackAXWindow(
+ for window: AlignerWindow,
+ processIdentifier: Int32,
+ in axMetadata: [AXWindowMetadata]
+ ) -> AXWindowMetadata? {
+ let normalizedTitle = normalizedWindowTitle(window.title)
+ guard !normalizedTitle.isEmpty else { return nil }
+
+ let finderCandidates = axMetadata.filter { metadata in
+ metadata.processIdentifier == processIdentifier
+ && normalizedWindowTitle(metadata.title ?? "") == normalizedTitle
+ && 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: AXActionResult?
+ let raiseAfterResult: AXActionResult?
+ 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
+ } else {
+ focusAfterResult = focusWindowResult(host.element, processIdentifier: processIdentifier)
+ raiseAfterResult = raiseWindowResult(host.element)
+ }
+
+ let didFocusOrRaise = focusBeforeResult.success
+ || raiseBeforeResult.success
+ || (focusAfterResult?.success ?? false)
+ || (raiseAfterResult?.success ?? false)
+ 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": focusAfterResult.map { String(describing: $0.error) } ?? "skippedAfterTabSelection",
+ "raiseAfterAXError": raiseAfterResult.map { String(describing: $0.error) } ?? "skippedAfterTabSelection",
+ "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 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
+ 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
+ }
+ }
+
+ if hostCandidates.count == 1 {
+ return hostCandidates[0]
+ }
+
+ DevelopmentDiagnostics.log("windowActivation.finderTab.hostAmbiguous", [
+ "windowID": window.id,
+ "candidateCount": hostCandidates.count,
+ "spaceIDCount": targetSpaceIDs.count,
+ "hasTargetFrame": targetFrame != nil,
+ "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
+ "targetTitleLength": window.title.count
+ ])
+ return nil
+ }
+
+ private func finderHostCanRepresentTarget(
+ _ window: AlignerWindow,
+ in host: AXWindowMetadata
+ ) -> Bool {
+ let normalizedTargetTitle = normalizedWindowTitle(window.title)
+ guard !normalizedTargetTitle.isEmpty else { return false }
+
+ if normalizedWindowTitle(host.title ?? "") == normalizedTargetTitle {
+ return true
+ }
+
+ return finderTabControl(matchingNormalizedTitle: normalizedTargetTitle, in: host.element) != 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 tabControl = finderTabControl(
+ 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
+ ])
+ if let processIdentifier = window.app.processIdentifier,
+ selectFinderWindowMenuItem(
+ processIdentifier: processIdentifier,
+ window: window,
+ normalizedTargetTitle: normalizedTargetTitle
+ ) {
+ return true
+ }
+ if let processIdentifier = window.app.processIdentifier,
+ selectFinderTabByCyclingTabs(
+ processIdentifier: processIdentifier,
+ host: host,
+ window: window,
+ normalizedTargetTitle: normalizedTargetTitle
+ ) {
+ return true
+ }
+ return false
+ }
+
+ let tabButton = tabControl.button
+ 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)
+ ])
+ if pressResult == .success,
+ finderHostTitleMatchesTarget(host, normalizedTargetTitle: normalizedTargetTitle) {
+ return true
+ }
+
+ if let tabGroup = tabControl.tabGroup {
+ let selectedChildrenResult = AXUIElementSetAttributeValue(
+ tabGroup,
+ kAXSelectedChildrenAttribute as CFString,
+ [tabButton] as CFArray
+ )
+ DevelopmentDiagnostics.log("windowActivation.finderTab.setSelectedChildren", [
+ "windowID": window.id,
+ "hostWindowID": host.windowID,
+ "axError": String(describing: selectedChildrenResult)
+ ])
+ if selectedChildrenResult == .success,
+ finderHostTitleMatchesTarget(host, normalizedTargetTitle: normalizedTargetTitle) {
+ return true
+ }
+ }
+
+ let valueResult = AXUIElementSetAttributeValue(
+ tabButton,
+ kAXValueAttribute as CFString,
+ kCFBooleanTrue
+ )
+ DevelopmentDiagnostics.log("windowActivation.finderTab.setValue", [
+ "windowID": window.id,
+ "hostWindowID": host.windowID,
+ "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
+ ) {
+ return true
+ }
+
+ if let processIdentifier = window.app.processIdentifier,
+ selectFinderTabByCyclingTabs(
+ processIdentifier: processIdentifier,
+ host: host,
+ window: window,
+ normalizedTargetTitle: 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? {
+ finderTabControl(matchingNormalizedTitle: targetTitle, in: root)?.button
+ }
+
+ private func finderTabControl(
+ matchingNormalizedTitle targetTitle: String,
+ in root: AXUIElement
+ ) -> FinderTabControl? {
+ var visitedCount = 0
+
+ func search(_ element: AXUIElement, depth: Int, tabGroup: AXUIElement?) -> FinderTabControl? {
+ 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 currentTabGroup = isTabGroup ? element : tabGroup
+ let isTabControl = subrole == "AXTabButton"
+ || (currentTabGroup != nil && (role == kAXRadioButtonRole as String || role == kAXButtonRole as String))
+
+ if normalizedWindowTitle(title ?? "") == targetTitle,
+ isTabControl {
+ return FinderTabControl(button: element, tabGroup: currentTabGroup)
+ }
+
+ guard isTabGroup || currentTabGroup != nil || depth < 7 else { return nil }
+ for child in axChildren(of: element) {
+ if let match = search(child, depth: depth + 1, tabGroup: currentTabGroup) {
+ return match
+ }
+ }
+
+ return nil
+ }
+
+ return search(root, depth: 0, tabGroup: nil)
+ }
+
+ private func finderHostTitleMatchesTarget(
+ _ host: AXWindowMetadata,
+ normalizedTargetTitle: String,
+ attempts: Int = 10,
+ delay: TimeInterval = 0.04
+ ) -> Bool {
+ for _ in 0..<attempts {
+ Thread.sleep(forTimeInterval: delay)
+ let selectedTitle = normalizedWindowTitle(
+ axStringAttribute(kAXTitleAttribute as String, for: host.element) ?? ""
+ )
+ if selectedTitle == normalizedTargetTitle {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ private func selectFinderWindowMenuItem(
+ processIdentifier: Int32,
+ window: AlignerWindow,
+ normalizedTargetTitle: String
+ ) -> Bool {
+ let appElement = AXUIElementCreateApplication(processIdentifier)
+ guard let menuBar = axElementAttribute(kAXMenuBarAttribute as String, for: appElement) else {
+ DevelopmentDiagnostics.log("windowActivation.finderTab.windowMenuMissing", [
+ "windowID": window.id,
+ "pid": processIdentifier
+ ])
+ return false
+ }
+
+ for menuBarItem in axChildren(of: menuBar) where isWindowMenuBarItem(menuBarItem) {
+ let openResult = AXUIElementPerformAction(menuBarItem, kAXPressAction as CFString)
+ Thread.sleep(forTimeInterval: 0.08)
+ guard openResult == .success,
+ let menuItem = finderWindowMenuItem(
+ matchingNormalizedTitle: normalizedTargetTitle,
+ in: menuBarItem
+ )
+ else {
+ continue
+ }
+
+ let pressResult = AXUIElementPerformAction(menuItem, kAXPressAction as CFString)
+ DevelopmentDiagnostics.log("windowActivation.finderTab.windowMenuPress", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "axError": String(describing: pressResult)
+ ])
+ guard pressResult == .success else { return false }
+
+ if finderFocusedWindowTitleMatches(
+ processIdentifier: processIdentifier,
+ window: window,
+ normalizedTargetTitle: normalizedTargetTitle,
+ initialDelay: 0.18,
+ attempts: 18,
+ delay: 0.06,
+ requiredConsecutiveMatches: 3
+ ) {
+ DevelopmentDiagnostics.log("windowActivation.finderTab.windowMenuSucceeded", [
+ "windowID": window.id,
+ "pid": processIdentifier
+ ])
+ return true
+ }
+
+ DevelopmentDiagnostics.log("windowActivation.finderTab.windowMenuVerificationFailed", [
+ "windowID": window.id,
+ "pid": processIdentifier
+ ])
+ }
+
+ DevelopmentDiagnostics.log("windowActivation.finderTab.windowMenuItemMissing", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
+ "targetTitleLength": window.title.count
+ ])
+ return false
+ }
+
+ private func isWindowMenuBarItem(_ element: AXUIElement) -> Bool {
+ let title = normalizedWindowTitle(axStringAttribute(kAXTitleAttribute as String, for: element) ?? "")
+ return title == "window" || title == "窗口"
+ }
+
+ private func finderWindowMenuItem(
+ matchingNormalizedTitle targetTitle: String,
+ in root: AXUIElement
+ ) -> AXUIElement? {
+ var visitedCount = 0
+
+ func search(_ element: AXUIElement, depth: Int) -> AXUIElement? {
+ visitedCount += 1
+ guard visitedCount <= 500, depth <= 8 else { return nil }
+
+ let role = axStringAttribute(kAXRoleAttribute as String, for: element)
+ let title = axStringAttribute(kAXTitleAttribute as String, for: element)
+ ?? axStringAttribute(kAXDescriptionAttribute as String, for: element)
+ if role == kAXMenuItemRole as String,
+ normalizedWindowTitle(title ?? "") == targetTitle {
+ return element
+ }
+
+ for child in axChildren(of: element) {
+ if let match = search(child, depth: depth + 1) {
+ return match
+ }
+ }
+
+ return nil
+ }
+
+ return search(root, depth: 0)
+ }
+
+ private func finderFocusedWindowTitleMatches(
+ processIdentifier: Int32,
+ window: AlignerWindow,
+ normalizedTargetTitle: String,
+ initialDelay: TimeInterval,
+ attempts: Int,
+ delay: TimeInterval,
+ requiredConsecutiveMatches: Int
+ ) -> 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)
+ }
+ lastTitle = title
+ if normalizedWindowTitle(title ?? "") == normalizedTargetTitle {
+ consecutiveMatches += 1
+ } else {
+ consecutiveMatches = 0
+ }
+
+ if consecutiveMatches >= requiredConsecutiveMatches {
+ return true
+ }
+ Thread.sleep(forTimeInterval: delay)
+ }
+
+ DevelopmentDiagnostics.log("windowActivation.finderTab.focusedTitleMismatch", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
+ "targetTitleLength": window.title.count,
+ "lastTitleHash": DevelopmentDiagnostics.stableFingerprint(lastTitle),
+ "lastTitleLength": lastTitle?.count
+ ])
+ return false
+ }
+
+ private func selectFinderTabByCyclingTabs(
+ processIdentifier: Int32,
+ host: AXWindowMetadata,
+ window: AlignerWindow,
+ normalizedTargetTitle: String
+ ) -> Bool {
+ let maximumAttempts = 24
+ for attempt in 1...maximumAttempts {
+ for shortcut in Self.finderNextTabShortcuts {
+ guard postKeyboardShortcut(to: processIdentifier, shortcut: shortcut) else {
+ DevelopmentDiagnostics.log("windowActivation.finderTab.cyclePostFailed", [
+ "windowID": window.id,
+ "hostWindowID": host.windowID,
+ "attempt": attempt,
+ "shortcut": shortcut.name
+ ])
+ return false
+ }
+
+ if finderHostTitleMatchesTarget(
+ host,
+ normalizedTargetTitle: normalizedTargetTitle,
+ attempts: 4,
+ delay: 0.035
+ ) {
+ DevelopmentDiagnostics.log("windowActivation.finderTab.cycleSucceeded", [
+ "windowID": window.id,
+ "hostWindowID": host.windowID,
+ "attempt": attempt,
+ "shortcut": shortcut.name
+ ])
+ return true
+ }
+ }
+ }
+
+ DevelopmentDiagnostics.log("windowActivation.finderTab.cycleFailed", [
+ "windowID": window.id,
+ "hostWindowID": host.windowID,
+ "attemptCount": maximumAttempts
+ ])
+ return false
+ }
+
+ private func postKeyboardShortcut(
+ to processIdentifier: Int32,
+ shortcut: KeyboardShortcut
+ ) -> Bool {
+ let source = CGEventSource(stateID: .combinedSessionState)
+ guard let keyDown = CGEvent(
+ keyboardEventSource: source,
+ virtualKey: shortcut.keyCode,
+ keyDown: true
+ ),
+ let keyUp = CGEvent(
+ keyboardEventSource: source,
+ virtualKey: shortcut.keyCode,
+ keyDown: false
+ )
+ else {
+ return false
+ }
+
+ keyDown.flags = shortcut.flags
+ keyUp.flags = shortcut.flags
+ keyDown.postToPid(processIdentifier)
+ keyUp.postToPid(processIdentifier)
+ return true
+ }
+
+ 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 axElementAttribute(_ attribute: String, for element: AXUIElement) -> AXUIElement? {
+ var value: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
+ let value,
+ CFGetTypeID(value) == AXUIElementGetTypeID()
+ else {
+ return nil
+ }
+
+ return (value as! AXUIElement)
+ }
+
+ 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 }
+ let overlap = (intersection.width * intersection.height) / denominator
+ let widthSimilarity = min(lhs.width, rhs.width) / max(lhs.width, rhs.width)
+ let heightSimilarity = min(lhs.height, rhs.height) / max(lhs.height, rhs.height)
+ let sizeSimilarity = min(widthSimilarity, heightSimilarity)
+ guard sizeSimilarity >= 0.80 else { return nil }
+ return min(overlap, sizeSimilarity)
+ }
+
+ private func cgWindowFingerprint(for window: AlignerWindow) -> AXWindowFingerprint? {
+ guard let rawWindows = CGWindowListCopyWindowInfo(
+ [.optionIncludingWindow],
+ CGWindowID(window.id)
+ ) as? [[String: Any]],
+ let rawWindow = rawWindows.first,
+ let ownerPID = int32Value(rawWindow[kCGWindowOwnerPID as String])
+ else {
+ return nil
+ }
+
+ return AXWindowFingerprint(
+ processIdentifier: ownerPID,
+ title: normalizedWindowTitle(title(rawWindow: rawWindow, axWindow: nil)),
+ size: bounds(rawWindow[kCGWindowBounds as String]).size
+ )
+ }
+
+ private func axWindowGeometryFingerprint(for window: AlignerWindow) -> AXWindowGeometryFingerprint? {
+ guard let processIdentifier = window.app.processIdentifier else { return nil }
+ let frame = matchingFrame(for: window)
+ return AXWindowGeometryFingerprint(
+ processIdentifier: processIdentifier,
+ title: normalizedWindowTitle(window.title),
+ frame: frame
+ )
+ }
+
+ private func cgWindowFrame(for window: AlignerWindow) -> CGRect? {
+ guard let rawWindows = CGWindowListCopyWindowInfo(
+ [.optionIncludingWindow],
+ CGWindowID(window.id)
+ ) as? [[String: Any]],
+ let rawWindow = rawWindows.first
+ else {
+ return nil
+ }
+
+ return bounds(rawWindow[kCGWindowBounds as String])
+ }
+
+ private func matchingFrame(for window: AlignerWindow) -> CGRect? {
+ if isFinderCGOnlyPageCandidate(window) {
+ return window.frame ?? cgWindowFrame(for: window)
+ }
+
+ return cgWindowFrame(for: window) ?? window.frame
+ }
+
+ private func matchingAXTitleCandidateCount(
+ for window: AlignerWindow,
+ in axMetadata: [AXWindowMetadata]
+ ) -> Int {
+ guard let processIdentifier = window.app.processIdentifier else { return 0 }
+ let normalizedTitle = normalizedWindowTitle(window.title)
+ return axMetadata.filter { metadata in
+ metadata.processIdentifier == processIdentifier
+ && normalizedWindowTitle(metadata.title ?? "") == normalizedTitle
+ }.count
+ }
+
+ private func matchingAXSizeCandidateCount(
+ for window: AlignerWindow,
+ in axMetadata: [AXWindowMetadata]
+ ) -> Int {
+ guard let processIdentifier = window.app.processIdentifier,
+ let cgFingerprint = cgWindowFingerprint(for: window)
+ else {
+ return 0
+ }
+
+ return axMetadata.filter { metadata in
+ metadata.processIdentifier == processIdentifier
+ && Int(metadata.size.width.rounded()) == cgFingerprint.width
+ && Int(metadata.size.height.rounded()) == cgFingerprint.height
+ }.count
+ }
+
+ func close(app: AlignerApp) throws -> WindowCloseResult {
+ DevelopmentDiagnostics.log("windowClose.app.start", [
+ "appName": app.name,
+ "bundleIdentifier": app.bundleIdentifier,
+ "pid": app.processIdentifier
+ ])
+
+ guard let processIdentifier = app.processIdentifier else {
+ DevelopmentDiagnostics.log("windowClose.app.missingPID", [
+ "appName": app.name,
+ "bundleIdentifier": app.bundleIdentifier
+ ])
+ return .appNotFound
+ }
+ guard let runningApplication = NSRunningApplication(processIdentifier: processIdentifier) else {
+ DevelopmentDiagnostics.log("windowClose.app.missingApplication", [
+ "appName": app.name,
+ "pid": processIdentifier
+ ])
+ return .appNotFound
+ }
+
+ let requested = runningApplication.terminate()
+ DevelopmentDiagnostics.log("windowClose.app.result", [
+ "appName": app.name,
+ "pid": processIdentifier,
+ "requested": requested
+ ])
+ return requested ? .requested : .failed("terminateFailed")
}
private func activateViaPrivateWindowServerAPI(_ window: AlignerWindow) -> PrivateWindowActivationOutcome? {
@@ -362,13 +1517,14 @@
return nil
}
+ let rawBounds = bounds(rawWindow[kCGWindowBounds as String])
let ownerPID = int32Value(rawWindow[kCGWindowOwnerPID as String])
let runningApplication = ownerPID.flatMap(NSRunningApplication.init(processIdentifier:))
let rawFingerprint = ownerPID.map {
AXWindowFingerprint(
processIdentifier: $0,
title: title(rawWindow: rawWindow, axWindow: nil),
- size: bounds(rawWindow[kCGWindowBounds as String]).size
+ size: rawBounds.size
)
}
let directAXWindow = axMetadata[windowID]
@@ -388,13 +1544,15 @@
app: app,
activationPolicy: activationPolicy(from: runningApplication),
title: title(rawWindow: rawWindow, axWindow: axWindow),
- size: bounds(rawWindow[kCGWindowBounds as String]).size,
+ size: rawBounds.size,
+ frame: rawBounds,
subrole: axWindow?.subrole ?? .standard,
isMinimized: axWindow?.isMinimized ?? false,
isFullscreen: !Set(spaceIDs).isDisjoint(with: fullscreenSpaceIDs),
isGhost: isGhost(
rawWindow: rawWindow,
axWindow: axWindow,
+ app: app,
spaceIDs: spaceIDs,
hasAXBackedTitleDuplicate: hasAXBackedTitleDuplicate
),
@@ -467,6 +1625,7 @@
title: axWindow.title ?? "",
identifierSource: identifierSource,
size: axWindow.size,
+ frame: axWindow.frame,
subrole: axWindow.subrole,
isMinimized: axWindow.isMinimized,
isFullscreen: !Set(spaceIDs).isDisjoint(with: fullscreenSpaceIDs),
@@ -481,7 +1640,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(
@@ -583,6 +1766,7 @@
private func isGhost(
rawWindow: [String: Any],
axWindow: AXWindowMetadata?,
+ app: AlignerApp,
spaceIDs: [UInt64],
hasAXBackedTitleDuplicate: Bool = false
) -> Bool {
@@ -596,10 +1780,20 @@
return false
}
if axWindow != nil {
- return layer != 0 || alpha <= 0
+ if alpha <= 0 {
+ return true
+ }
+ if layer == 0 {
+ return false
+ }
+
+ return !isOnscreen
}
if hasAXBackedTitleDuplicate {
return true
+ }
+ if isKnownCGOnlyMainWindow(rawWindow: rawWindow, app: app) {
+ return false
}
if !spaceIDs.isEmpty, isLikelyFullscreenAuxiliaryCGWindow(bounds: rawBounds) {
return true
@@ -616,6 +1810,40 @@
private func isLikelyFullscreenAuxiliaryCGWindow(bounds: CGRect) -> Bool {
bounds.width < 500 || bounds.height < 160
+ }
+
+ private func isKnownCGOnlyMainWindow(rawWindow: [String: Any], app: AlignerApp) -> Bool {
+ let layer = intValue(rawWindow[kCGWindowLayer as String]) ?? 0
+ let alpha = doubleValue(rawWindow[kCGWindowAlpha as String]) ?? 1
+ let isOnscreen = boolValue(rawWindow[kCGWindowIsOnscreen as String])
+ let title = stringValue(rawWindow[kCGWindowName as String]) ?? ""
+ let rawBounds = bounds(rawWindow[kCGWindowBounds as String])
+ let trimmedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
+
+ if app.bundleIdentifier == "cc.ffitch.shottr" {
+ return isOnscreen
+ && alpha > 0
+ && trimmedTitle == "Shottr"
+ && rawBounds.width > WindowEnumerationPolicy.minimumVisibleWidth
+ && rawBounds.height > WindowEnumerationPolicy.minimumVisibleHeight
+ }
+
+ if app.bundleIdentifier == "com.apple.finder" {
+ return layer == 0
+ && alpha > 0
+ && !trimmedTitle.isEmpty
+ && rawBounds.width > WindowEnumerationPolicy.minimumVisibleWidth
+ && rawBounds.height > WindowEnumerationPolicy.minimumVisibleHeight
+ }
+
+ guard app.bundleIdentifier == "io.dcloud.HBuilderX" else { return false }
+
+ return layer == 0
+ && alpha > 0
+ && !isOnscreen
+ && !trimmedTitle.isEmpty
+ && rawBounds.width > WindowEnumerationPolicy.minimumVisibleWidth
+ && rawBounds.height > WindowEnumerationPolicy.minimumVisibleHeight
}
private func isDesktopElement(rawWindow: [String: Any], app: AlignerApp) -> Bool {
@@ -654,6 +1882,14 @@
return "activationFailed"
}
}
+
+ private static let commandWVirtualKeyCode: CGKeyCode = 13
+ private static let rightBracketVirtualKeyCode: CGKeyCode = 30
+ private static let tabVirtualKeyCode: CGKeyCode = 48
+ private static let finderNextTabShortcuts: [KeyboardShortcut] = [
+ KeyboardShortcut(name: "controlTab", keyCode: tabVirtualKeyCode, flags: .maskControl),
+ KeyboardShortcut(name: "commandShiftRightBracket", keyCode: rightBracketVirtualKeyCode, flags: [.maskCommand, .maskShift])
+ ]
}
private struct AXWindowMetadata {
@@ -663,6 +1899,7 @@
let title: String?
let isMinimized: Bool
let size: CGSize
+ let frame: CGRect?
let subrole: AlignerWindowSubrole
let hasExplicitSubrole: Bool
let isInteractable: Bool
@@ -722,6 +1959,34 @@
}
}
+private struct AXWindowGeometryFingerprint: Hashable {
+ let processIdentifier: Int32
+ let title: String
+ let x: Int
+ let y: Int
+ let width: Int
+ let height: Int
+
+ init?(processIdentifier: Int32, title: String, frame: CGRect?) {
+ guard let frame else { return nil }
+
+ self.processIdentifier = processIdentifier
+ self.title = title
+ self.x = Int(frame.origin.x.rounded())
+ self.y = Int(frame.origin.y.rounded())
+ self.width = Int(frame.size.width.rounded())
+ self.height = Int(frame.size.height.rounded())
+ }
+
+ init?(_ metadata: AXWindowMetadata) {
+ self.init(
+ processIdentifier: metadata.processIdentifier,
+ title: normalizedWindowTitle(metadata.title ?? ""),
+ frame: metadata.frame
+ )
+ }
+}
+
private struct RestorableAXWindow {
let processIdentifier: Int32
let element: AXUIElement
@@ -749,22 +2014,21 @@
let app = app(from: application, appCategorizer: appCategorizer)
let appElement = AXUIElementCreateApplication(application.processIdentifier)
- var windowsValue: CFTypeRef?
- guard AXUIElementCopyAttributeValue(appElement, kAXWindowsAttribute as CFString, &windowsValue) == .success,
- let windows = windowsValue as? [AXUIElement]
- else {
- continue
- }
+ let windows = windows(for: appElement)
+ guard !windows.isEmpty else { continue }
for (windowIndex, window) in windows.enumerated() {
let subrole = subrole(for: window)
+ let size = size(for: window)
+ let position = position(for: window)
result.append(AXWindowMetadata(
windowID: windowID(for: window),
app: app,
activationPolicy: activationPolicy(from: application),
title: title(for: window),
isMinimized: boolAttribute(kAXMinimizedAttribute as String, for: window) ?? false,
- size: size(for: window),
+ size: size,
+ frame: position.map { CGRect(origin: $0, size: size) },
subrole: subrole.value,
hasExplicitSubrole: subrole.isExplicit,
isInteractable: boolAttribute(kAXEnabledAttribute as String, for: window) ?? true,
@@ -773,6 +2037,38 @@
element: window
))
}
+ }
+
+ return result
+ }
+
+ private static func windows(for appElement: AXUIElement) -> [AXUIElement] {
+ var result: [AXUIElement] = []
+ var seen = Set<AXWindowElementKey>()
+
+ func append(_ window: AXUIElement) {
+ let key = AXWindowElementKey(window)
+ guard !seen.contains(key) else { return }
+ seen.insert(key)
+ result.append(window)
+ }
+
+ var windowsValue: CFTypeRef?
+ if AXUIElementCopyAttributeValue(appElement, kAXWindowsAttribute as CFString, &windowsValue) == .success,
+ let windows = windowsValue as? [AXUIElement] {
+ windows.forEach(append)
+ }
+
+ for attribute in [kAXFocusedWindowAttribute, kAXMainWindowAttribute] {
+ var windowValue: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(appElement, attribute as CFString, &windowValue) == .success,
+ let window = windowValue,
+ CFGetTypeID(window) == AXUIElementGetTypeID()
+ else {
+ continue
+ }
+
+ append(window as! AXUIElement)
}
return result
@@ -859,6 +2155,23 @@
return size
}
+ private static func position(for window: AXUIElement) -> CGPoint? {
+ var value: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(window, kAXPositionAttribute as CFString, &value) == .success,
+ let axValue = value,
+ CFGetTypeID(axValue) == AXValueGetTypeID()
+ else {
+ return nil
+ }
+
+ var position = CGPoint.zero
+ guard AXValueGetValue(axValue as! AXValue, .cgPoint, &position) else {
+ return nil
+ }
+
+ return position
+ }
+
private static func subrole(for window: AXUIElement) -> (value: AlignerWindowSubrole, isExplicit: Bool) {
var value: CFTypeRef?
guard AXUIElementCopyAttributeValue(window, kAXSubroleAttribute as CFString, &value) == .success,
@@ -877,6 +2190,39 @@
default:
return (.unknown(subrole), true)
}
+ }
+}
+
+private struct AXWindowElementKey: Hashable {
+ private let typeID: CFTypeID
+ private let pointer: UInt
+ private let windowID: UInt32?
+
+ init(_ element: AXUIElement) {
+ self.typeID = CFGetTypeID(element)
+ self.pointer = UInt(bitPattern: Unmanaged.passUnretained(element).toOpaque())
+ self.windowID = PrivateAXWindowIDReader.windowID(for: element)
+ }
+
+ static func == (lhs: AXWindowElementKey, rhs: AXWindowElementKey) -> Bool {
+ if let lhsWindowID = lhs.windowID,
+ let rhsWindowID = rhs.windowID {
+ return lhsWindowID == rhsWindowID
+ }
+
+ return lhs.typeID == rhs.typeID && lhs.pointer == rhs.pointer
+ }
+
+ func hash(into hasher: inout Hasher) {
+ if let windowID {
+ hasher.combine("windowID")
+ hasher.combine(windowID)
+ return
+ }
+
+ hasher.combine("pointer")
+ hasher.combine(typeID)
+ hasher.combine(pointer)
}
}
@@ -918,6 +2264,10 @@
value as? String
}
+private func normalizedWindowTitle(_ title: String) -> String {
+ title.trimmingCharacters(in: .whitespacesAndNewlines)
+}
+
private func boolValue(_ value: Any?) -> Bool {
switch value {
case let bool as Bool:
--
Gitblit v1.9.3