import AppKit
|
@preconcurrency import ApplicationServices
|
import CoreGraphics
|
import Foundation
|
import AlignerCore
|
|
final class CGWindowAXWindowService: WindowServiceProtocol, WindowRestorationServiceProtocol, WindowActivationServiceProtocol, WindowCloseServiceProtocol {
|
weak var delegate: WindowServiceDelegate?
|
|
private let appCategorizer: any AppCategorizer
|
private let spaceIDsByWindowIDProvider: ([UInt32]) -> [UInt32: [UInt64]]
|
private let fullscreenSpaceIDs: Set<UInt64>
|
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
|
}
|
|
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(),
|
fullscreenSpaceIDs: Set<UInt64> = [],
|
privateActivationBridge: PrivateWindowActivationBridge? = .shared,
|
privateSpaceActivationBridge: PrivateSpaceActivationBridge? = .shared
|
) {
|
self.appCategorizer = appCategorizer
|
self.fullscreenSpaceIDs = fullscreenSpaceIDs
|
self.spaceIDsByWindowIDProvider = spaceIDsByWindowIDProvider
|
self.privateActivationBridge = privateActivationBridge
|
self.privateSpaceActivationBridge = privateSpaceActivationBridge
|
}
|
|
func allWindows() throws -> [AlignerWindow] {
|
let records = try windowRecords()
|
return records.compactMap(WindowEnumerationPolicy.window)
|
}
|
|
func windows(for space: AlignerSpace) throws -> [AlignerWindow] {
|
try allWindows().filter { window in
|
window.spaceIDs.contains(space.id)
|
}
|
}
|
|
func refresh() {}
|
|
func startObserving() {}
|
|
func stopObserving() {}
|
|
func restore(windowID: UInt32) throws -> WindowRestoreResult {
|
guard let restorableWindow = restorableAXWindowsByID[windowID] else {
|
DevelopmentDiagnostics.log("windowActivation.restore.windowNotFound", [
|
"windowID": windowID
|
])
|
return .windowNotFound
|
}
|
DevelopmentDiagnostics.log("windowActivation.restore.start", [
|
"windowID": windowID,
|
"pid": restorableWindow.processIdentifier
|
])
|
|
let minimizeResult = AXUIElementSetAttributeValue(
|
restorableWindow.element,
|
kAXMinimizedAttribute as CFString,
|
kCFBooleanFalse
|
)
|
guard minimizeResult == .success else {
|
DevelopmentDiagnostics.log("windowActivation.restore.setMinimizedFailed", [
|
"windowID": windowID,
|
"pid": restorableWindow.processIdentifier,
|
"axError": String(describing: minimizeResult)
|
])
|
throw AXWindowRestoreError.setMinimizedFailed(minimizeResult)
|
}
|
|
let beforeActivateAttempt = focusMainRaiseWindowResult(
|
restorableWindow.element,
|
processIdentifier: restorableWindow.processIdentifier
|
)
|
|
guard activateApplication(processIdentifier: restorableWindow.processIdentifier) else {
|
DevelopmentDiagnostics.log("windowActivation.restore.activateApplicationFailed", [
|
"windowID": windowID,
|
"pid": restorableWindow.processIdentifier,
|
"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 afterActivateAttempt = focusMainRaiseWindowResult(
|
restorableWindow.element,
|
processIdentifier: restorableWindow.processIdentifier
|
)
|
|
let restored = beforeActivateAttempt.success || afterActivateAttempt.success
|
DevelopmentDiagnostics.log("windowActivation.restore.end", [
|
"windowID": windowID,
|
"pid": restorableWindow.processIdentifier,
|
"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 restored ? .restored : .activationFailed
|
}
|
|
func activate(window: AlignerWindow) throws -> WindowActivationResult {
|
DevelopmentDiagnostics.log("windowActivation.activate.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,
|
"isMinimized": window.isMinimized,
|
"isFullscreen": window.isFullscreen
|
])
|
if window.isMinimized {
|
let refreshedWindows = try allWindows()
|
DevelopmentDiagnostics.log("windowActivation.activate.refreshedBeforeRestore", [
|
"windowID": window.id,
|
"refreshedWindowCount": refreshedWindows.count
|
])
|
let restoreResult = try restore(windowID: window.id)
|
DevelopmentDiagnostics.log("windowActivation.activate.restoreResult", [
|
"windowID": window.id,
|
"result": Self.restoreResultString(restoreResult)
|
])
|
switch restoreResult {
|
case .restored:
|
return .restored
|
case .windowNotFound:
|
return activateApplication(for: window) ? .activationFailed : .windowNotFound
|
case .unsupported:
|
return .unsupported
|
case .activationFailed:
|
return .activationFailed
|
}
|
}
|
|
let spaceFocusOutcome = focusTargetSpaceIfNeeded(for: window)
|
if spaceFocusOutcome?.didRequestFocus == true {
|
Thread.sleep(forTimeInterval: 0.12)
|
}
|
|
let privateActivationOutcome = activateViaPrivateWindowServerAPI(window)
|
let didPrivatelyActivate = privateActivationOutcome?.succeeded == true
|
|
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,
|
"titleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
|
"titleLength": window.title.count,
|
"titleIsEmpty": window.title.isEmpty,
|
"pid": window.app.processIdentifier,
|
"found": axWindow != nil,
|
"matchedWindowID": axWindow?.windowID,
|
"matchedTitleHash": DevelopmentDiagnostics.stableFingerprint(axWindow?.title),
|
"matchedTitleLength": axWindow?.title?.count,
|
"matchedTitleIsEmpty": axWindow?.title?.isEmpty,
|
"matchedPID": axWindow?.processIdentifier,
|
"spaceFocusTargetSpaceID": spaceFocusOutcome?.targetSpaceID,
|
"spaceFocusDisplayIdentifier": spaceFocusOutcome?.displayIdentifier,
|
"spaceFocusPreviousSpaceID": spaceFocusOutcome?.previousCurrentSpaceID,
|
"spaceFocusRequested": spaceFocusOutcome?.didRequestFocus,
|
"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)
|
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,
|
"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 || retrySucceeded {
|
return .activated
|
}
|
|
return activated ? .appActivatedOnly : .windowNotFound
|
}
|
|
let beforeActivateAttempt = focusMainRaiseWindowResult(
|
axWindow.element,
|
processIdentifier: axWindow.processIdentifier
|
)
|
|
guard didPrivatelyActivate || activateApplication(for: window) else {
|
DevelopmentDiagnostics.log("windowActivation.activate.applicationActivationFailed", [
|
"windowID": window.id,
|
"pid": window.app.processIdentifier,
|
"privateActivated": didPrivatelyActivate,
|
"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 afterActivateAttempt = focusMainRaiseWindowResult(
|
axWindow.element,
|
processIdentifier: axWindow.processIdentifier
|
)
|
let retryAttempt = (didPrivatelyActivate || beforeActivateAttempt.success || afterActivateAttempt.success)
|
? nil
|
: retryFocusMainRaiseWindow(
|
for: window,
|
delay: 0.10,
|
matchAttempt: "postActivateRetry"
|
)
|
|
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": 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")
|
])
|
if finderPreciseActivationFailed {
|
return .activationFailed
|
}
|
return activated ? .activated : .appActivatedOnly
|
}
|
|
func close(window: AlignerWindow) throws -> WindowCloseResult {
|
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 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
|
}
|
|
let budget = FinderTabActivationBudget(limit: Self.finderTabActivationBudgetLimit)
|
let hostLookupStartedAt = ProcessInfo.processInfo.systemUptime
|
let host = finderTabHostAXWindow(for: window, in: axMetadata, budget: budget)
|
DevelopmentDiagnostics.log("\(operation).finderTab.hostLookup", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"reason": reason,
|
"hostFound": host != nil,
|
"hostWindowID": host?.windowID,
|
"elapsedMilliseconds": Self.elapsedMilliseconds(since: hostLookupStartedAt),
|
"overallElapsedMilliseconds": budget.elapsedMilliseconds,
|
"budgetMilliseconds": Self.milliseconds(budget.limit),
|
"budgetRemainingMilliseconds": Self.milliseconds(budget.remaining)
|
])
|
|
guard let host else {
|
if budget.isExhausted {
|
DevelopmentDiagnostics.log("\(operation).finderTab.hostMissingBudgetExceeded", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"reason": reason,
|
"elapsedMilliseconds": budget.elapsedMilliseconds,
|
"budgetMilliseconds": Self.milliseconds(budget.limit)
|
])
|
return false
|
}
|
|
let normalizedTargetTitle = normalizedWindowTitle(window.title)
|
let applicationActivated = activateApplication(processIdentifier: processIdentifier)
|
let selectedViaWindowMenu = !normalizedTargetTitle.isEmpty
|
&& selectFinderWindowMenuItem(
|
processIdentifier: processIdentifier,
|
window: window,
|
normalizedTargetTitle: normalizedTargetTitle,
|
budget: budget
|
)
|
DevelopmentDiagnostics.log("\(operation).finderTab.hostlessWindowMenuResult", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"reason": reason,
|
"applicationActivated": applicationActivated,
|
"selectedViaWindowMenu": selectedViaWindowMenu,
|
"spaceIDs": window.spaceIDs,
|
"targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
|
"targetTitleLength": window.title.count,
|
"elapsedMilliseconds": budget.elapsedMilliseconds,
|
"budgetMilliseconds": Self.milliseconds(budget.limit)
|
])
|
if selectedViaWindowMenu {
|
return true
|
}
|
|
DevelopmentDiagnostics.log("\(operation).finderTab.hostMissing", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"reason": reason,
|
"spaceIDs": window.spaceIDs,
|
"elapsedMilliseconds": budget.elapsedMilliseconds,
|
"budgetMilliseconds": Self.milliseconds(budget.limit)
|
])
|
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 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.
|
afterAttempt = nil
|
} else {
|
afterAttempt = focusMainRaiseWindowResult(host.element, processIdentifier: processIdentifier)
|
}
|
|
let didFocusOrRaise = beforeAttempt.success || (afterAttempt?.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: beforeAttempt.focused.error),
|
"mainBeforeAXError": String(describing: beforeAttempt.main.error),
|
"raiseBeforeAXError": String(describing: beforeAttempt.raised.error),
|
"focusAfterAXError": afterAttempt.map { String(describing: $0.focused.error) } ?? "skippedAfterTabSelection",
|
"mainAfterAXError": afterAttempt.map { String(describing: $0.main.error) } ?? "skippedAfterTabSelection",
|
"raiseAfterAXError": afterAttempt.map { String(describing: $0.raised.error) } ?? "skippedAfterTabSelection",
|
"activated": activated,
|
"elapsedMilliseconds": budget.elapsedMilliseconds,
|
"budgetMilliseconds": Self.milliseconds(budget.limit),
|
"budgetExceeded": budget.isExhausted
|
])
|
|
return activated
|
}
|
|
private func 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],
|
budget: FinderTabActivationBudget
|
) -> 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 targetFrame = matchingFrame(for: window)
|
let frameScoredHosts = spaceScopedCandidates.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 = 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 = frameScoredHosts[1]
|
if best.score - secondBest.score >= 0.05 {
|
DevelopmentDiagnostics.log("windowActivation.finderTab.frameInferredHost", [
|
"windowID": window.id,
|
"hostWindowID": best.metadata.windowID,
|
"score": Double(best.score),
|
"secondBestScore": Double(secondBest.score),
|
"spaceIDCount": targetSpaceIDs.count,
|
"candidateCount": spaceScopedCandidates.count,
|
"targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
|
"targetTitleLength": window.title.count
|
])
|
return best.metadata
|
}
|
}
|
|
guard !budget.isExhausted else {
|
DevelopmentDiagnostics.log("windowActivation.finderTab.hostLookupBudgetExceeded", [
|
"windowID": window.id,
|
"candidateCount": spaceScopedCandidates.count,
|
"spaceIDCount": targetSpaceIDs.count,
|
"elapsedMilliseconds": budget.elapsedMilliseconds,
|
"budgetMilliseconds": Self.milliseconds(budget.limit)
|
])
|
return nil
|
}
|
|
let representationScanCandidates = Array(spaceScopedCandidates.prefix(Self.finderTabHostRepresentationScanLimit))
|
let selectableHostCandidates = representationScanCandidates.filter { metadata in
|
finderHostCanRepresentTarget(window, in: metadata, budget: budget)
|
}
|
|
if spaceScopedCandidates.count > representationScanCandidates.count {
|
DevelopmentDiagnostics.log("windowActivation.finderTab.hostRepresentationScanLimited", [
|
"windowID": window.id,
|
"candidateCount": spaceScopedCandidates.count,
|
"scannedCandidateCount": representationScanCandidates.count,
|
"selectableCandidateCount": selectableHostCandidates.count,
|
"elapsedMilliseconds": budget.elapsedMilliseconds,
|
"budgetMilliseconds": Self.milliseconds(budget.limit)
|
])
|
}
|
|
let hostCandidates = selectableHostCandidates.isEmpty
|
? spaceScopedCandidates
|
: selectableHostCandidates
|
|
if hostCandidates.count == 1 {
|
if selectableHostCandidates.isEmpty {
|
DevelopmentDiagnostics.log("windowActivation.finderTab.singleFrameHostFallback", [
|
"windowID": window.id,
|
"hostWindowID": hostCandidates[0].windowID,
|
"spaceIDCount": targetSpaceIDs.count,
|
"targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
|
"targetTitleLength": window.title.count
|
])
|
}
|
return hostCandidates[0]
|
}
|
|
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,
|
budget: FinderTabActivationBudget
|
) -> Bool {
|
let normalizedTargetTitle = normalizedWindowTitle(window.title)
|
guard !normalizedTargetTitle.isEmpty else { return false }
|
|
if normalizedWindowTitle(host.title ?? "") == normalizedTargetTitle {
|
return true
|
}
|
|
guard !budget.isExhausted else { return false }
|
return finderTabControl(matchingNormalizedTitle: normalizedTargetTitle, in: host.element, budget: budget) != nil
|
}
|
|
private func selectFinderTab(
|
_ window: AlignerWindow,
|
in host: AXWindowMetadata,
|
budget: FinderTabActivationBudget
|
) -> 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,
|
budget: budget
|
) else {
|
DevelopmentDiagnostics.log("windowActivation.finderTab.tabButtonMissing", [
|
"windowID": window.id,
|
"hostWindowID": host.windowID,
|
"targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
|
"targetTitleLength": window.title.count,
|
"elapsedMilliseconds": budget.elapsedMilliseconds,
|
"budgetMilliseconds": Self.milliseconds(budget.limit)
|
])
|
if let processIdentifier = window.app.processIdentifier,
|
!budget.isExhausted,
|
selectFinderTabByCyclingTabs(
|
processIdentifier: processIdentifier,
|
host: host,
|
window: window,
|
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
|
}
|
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,
|
attempts: Self.finderTabFastTitleMatchAttempts,
|
delay: Self.finderTabFastTitleMatchDelay,
|
budget: budget
|
) {
|
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,
|
attempts: Self.finderTabFastTitleMatchAttempts,
|
delay: Self.finderTabFastTitleMatchDelay,
|
budget: budget
|
) {
|
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,
|
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,
|
budget: budget
|
) {
|
return true
|
}
|
|
if let processIdentifier = window.app.processIdentifier,
|
!budget.isExhausted,
|
selectFinderWindowMenuItem(
|
processIdentifier: processIdentifier,
|
window: window,
|
normalizedTargetTitle: normalizedTargetTitle,
|
budget: budget
|
) {
|
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,
|
"elapsedMilliseconds": budget.elapsedMilliseconds,
|
"budgetMilliseconds": Self.milliseconds(budget.limit),
|
"budgetExceeded": budget.isExhausted
|
])
|
return false
|
}
|
|
private func finderTabControl(
|
matchingNormalizedTitle targetTitle: String,
|
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, !budget.isExhausted 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,
|
budget: FinderTabActivationBudget? = nil
|
) -> Bool {
|
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
|
}
|
}
|
|
return false
|
}
|
|
private func selectFinderWindowMenuItem(
|
processIdentifier: Int32,
|
window: AlignerWindow,
|
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", [
|
"windowID": window.id,
|
"pid": processIdentifier
|
])
|
return false
|
}
|
|
for menuBarItem in axChildren(of: menuBar) where isWindowMenuBarItem(menuBarItem) {
|
let openResult = AXUIElementPerformAction(menuBarItem, kAXPressAction as CFString)
|
if budget.remaining > 0 {
|
Thread.sleep(forTimeInterval: min(0.08, budget.remaining))
|
}
|
guard openResult == .success,
|
let menuItem = finderWindowMenuItem(
|
matchingNormalizedTitle: normalizedTargetTitle,
|
in: menuBarItem,
|
budget: budget
|
)
|
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: Self.finderTabWindowMenuVerificationInitialDelay,
|
attempts: Self.finderTabWindowMenuVerificationAttempts,
|
delay: Self.finderTabWindowMenuVerificationDelay,
|
requiredConsecutiveMatches: 2,
|
budget: budget
|
) {
|
DevelopmentDiagnostics.log("windowActivation.finderTab.windowMenuSucceeded", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"elapsedMilliseconds": budget.elapsedMilliseconds,
|
"budgetMilliseconds": Self.milliseconds(budget.limit)
|
])
|
return true
|
}
|
|
DevelopmentDiagnostics.log("windowActivation.finderTab.windowMenuVerificationFailed", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"elapsedMilliseconds": budget.elapsedMilliseconds,
|
"budgetMilliseconds": Self.milliseconds(budget.limit),
|
"budgetExceeded": budget.isExhausted
|
])
|
}
|
|
DevelopmentDiagnostics.log("windowActivation.finderTab.windowMenuItemMissing", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
|
"targetTitleLength": window.title.count,
|
"elapsedMilliseconds": budget.elapsedMilliseconds,
|
"budgetMilliseconds": Self.milliseconds(budget.limit),
|
"budgetExceeded": budget.isExhausted
|
])
|
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,
|
budget: FinderTabActivationBudget
|
) -> AXUIElement? {
|
var visitedCount = 0
|
|
func search(_ element: AXUIElement, depth: Int) -> AXUIElement? {
|
visitedCount += 1
|
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)
|
?? 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 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,
|
normalizedTargetTitle: String,
|
initialDelay: TimeInterval,
|
attempts: Int,
|
delay: TimeInterval,
|
requiredConsecutiveMatches: Int,
|
budget: FinderTabActivationBudget
|
) -> Bool {
|
var consecutiveMatches = 0
|
var lastTitle: String?
|
|
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
|
} else {
|
consecutiveMatches = 0
|
}
|
|
if consecutiveMatches >= requiredConsecutiveMatches {
|
return true
|
}
|
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", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
|
"targetTitleLength": window.title.count,
|
"lastTitleHash": DevelopmentDiagnostics.stableFingerprint(lastTitle),
|
"lastTitleLength": lastTitle?.count,
|
"elapsedMilliseconds": budget.elapsedMilliseconds,
|
"budgetMilliseconds": Self.milliseconds(budget.limit),
|
"budgetExceeded": budget.isExhausted
|
])
|
return false
|
}
|
|
private func selectFinderTabByCyclingTabs(
|
processIdentifier: Int32,
|
host: AXWindowMetadata,
|
window: AlignerWindow,
|
normalizedTargetTitle: String,
|
budget: FinderTabActivationBudget
|
) -> Bool {
|
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", [
|
"windowID": window.id,
|
"hostWindowID": host.windowID,
|
"attempt": attempt,
|
"shortcut": shortcut.name
|
])
|
return false
|
}
|
|
if finderHostTitleMatchesTarget(
|
host,
|
normalizedTargetTitle: normalizedTargetTitle,
|
attempts: 2,
|
delay: 0.025,
|
budget: budget
|
) {
|
DevelopmentDiagnostics.log("windowActivation.finderTab.cycleSucceeded", [
|
"windowID": window.id,
|
"hostWindowID": host.windowID,
|
"attempt": attempt,
|
"attemptLimit": maximumAttempts,
|
"shortcut": shortcut.name,
|
"elapsedMilliseconds": budget.elapsedMilliseconds,
|
"budgetMilliseconds": Self.milliseconds(budget.limit)
|
])
|
return true
|
}
|
}
|
}
|
|
DevelopmentDiagnostics.log("windowActivation.finderTab.cycleFailed", [
|
"windowID": window.id,
|
"hostWindowID": host.windowID,
|
"attemptCount": maximumAttempts,
|
"elapsedMilliseconds": budget.elapsedMilliseconds,
|
"budgetMilliseconds": Self.milliseconds(budget.limit),
|
"budgetExceeded": budget.isExhausted
|
])
|
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? {
|
guard window.identifierSource == .cgWindow,
|
let processIdentifier = window.app.processIdentifier,
|
let privateActivationBridge
|
else {
|
DevelopmentDiagnostics.log("windowActivation.privateActivation.skipped", [
|
"windowID": window.id,
|
"identifierSource": String(describing: window.identifierSource),
|
"hasPID": window.app.processIdentifier != nil,
|
"bridgeAvailable": privateActivationBridge != nil
|
])
|
return nil
|
}
|
|
let outcome = privateActivationBridge.activate(
|
processIdentifier: processIdentifier,
|
windowID: window.id
|
)
|
DevelopmentDiagnostics.log("windowActivation.privateActivation.result", [
|
"windowID": window.id,
|
"pid": processIdentifier,
|
"getProcessStatus": outcome.getProcessStatus,
|
"setFrontError": outcome.setFrontError.map(String.init(describing:)),
|
"makeKeyFirstError": outcome.makeKeyFirstError.map(String.init(describing:)),
|
"makeKeySecondError": outcome.makeKeySecondError.map(String.init(describing:)),
|
"succeeded": outcome.succeeded
|
])
|
return outcome
|
}
|
|
private func focusTargetSpaceIfNeeded(for window: AlignerWindow) -> PrivateSpaceFocusOutcome? {
|
guard window.identifierSource == .cgWindow,
|
!window.spaceIDs.isEmpty,
|
let privateSpaceActivationBridge
|
else {
|
DevelopmentDiagnostics.log("windowActivation.spaceFocus.skipped", [
|
"windowID": window.id,
|
"identifierSource": String(describing: window.identifierSource),
|
"spaceIDCount": window.spaceIDs.count,
|
"bridgeAvailable": privateSpaceActivationBridge != nil
|
])
|
return nil
|
}
|
|
let outcome = privateSpaceActivationBridge.focus(spaceIDs: window.spaceIDs)
|
DevelopmentDiagnostics.log("windowActivation.spaceFocus.result", [
|
"windowID": window.id,
|
"targetSpaceID": outcome?.targetSpaceID,
|
"displayIdentifier": outcome?.displayIdentifier,
|
"previousCurrentSpaceID": outcome?.previousCurrentSpaceID,
|
"didRequestFocus": outcome?.didRequestFocus,
|
"error": outcome?.error
|
])
|
return outcome
|
}
|
|
private func windowRecords() throws -> [WindowEnumerationRecord] {
|
guard let rawWindows = CGWindowListCopyWindowInfo(
|
[.optionAll, .excludeDesktopElements],
|
kCGNullWindowID
|
) as? [[String: Any]] else {
|
return []
|
}
|
|
let axWindows = AXWindowMetadataReader.metadata(appCategorizer: appCategorizer)
|
var axMetadata: [UInt32: AXWindowMetadata] = [:]
|
for metadata in axWindows {
|
if let windowID = metadata.windowID {
|
axMetadata[windowID] = metadata
|
}
|
}
|
let axBackedTitleKeys = Set(axMetadata.values.compactMap(AXWindowTitleKey.init))
|
let cgWindowIDs = Set(rawWindows.compactMap { uint32Value($0[kCGWindowNumber as String]) })
|
var fallbackAXMetadataByFingerprint: [AXWindowFingerprint: [AXWindowMetadata]] = [:]
|
for metadata in axWindows {
|
if let windowID = metadata.windowID, cgWindowIDs.contains(windowID) {
|
continue
|
}
|
|
fallbackAXMetadataByFingerprint[AXWindowFingerprint(metadata), default: []].append(metadata)
|
}
|
let windowIDs = Array(cgWindowIDs.union(axMetadata.keys))
|
let spaceIDsByWindowID = spaceIDsByWindowIDProvider(windowIDs)
|
let cgWindowFingerprints = Set(rawWindows.compactMap { rawWindow -> AXWindowFingerprint? in
|
guard let ownerPID = int32Value(rawWindow[kCGWindowOwnerPID as String]) else {
|
return nil
|
}
|
|
let windowID = uint32Value(rawWindow[kCGWindowNumber as String])
|
let axWindow = windowID.flatMap { axMetadata[$0] }
|
return AXWindowFingerprint(
|
processIdentifier: ownerPID,
|
title: title(rawWindow: rawWindow, axWindow: axWindow),
|
size: bounds(rawWindow[kCGWindowBounds as String]).size
|
)
|
})
|
var occupiedWindowIDs = Set(windowIDs)
|
var nextRestorableAXWindowsByID: [UInt32: RestorableAXWindow] = [:]
|
var consumedAXMetadata = Set<AXWindowIdentity>()
|
|
let cgRecords: [WindowEnumerationRecord] = rawWindows.compactMap { rawWindow in
|
guard let windowID = uint32Value(rawWindow[kCGWindowNumber as String]) else {
|
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: rawBounds.size
|
)
|
}
|
let directAXWindow = axMetadata[windowID]
|
let fallbackAXWindow = rawFingerprint.flatMap { fingerprint in
|
fallbackAXMetadataByFingerprint[fingerprint]?.first { metadata in
|
!consumedAXMetadata.contains(AXWindowIdentity(metadata))
|
}
|
}
|
let axWindow = directAXWindow ?? fallbackAXWindow
|
let spaceIDs = spaceIDsByWindowID[windowID] ?? []
|
let hasAXBackedTitleDuplicate = axWindow == nil
|
&& ownerPID.flatMap { AXWindowTitleKey(processIdentifier: $0, title: title(rawWindow: rawWindow, axWindow: nil)) }
|
.map { axBackedTitleKeys.contains($0) } == true
|
let app = app(from: runningApplication, rawWindow: rawWindow, ownerPID: ownerPID)
|
let record = WindowEnumerationRecord(
|
id: windowID,
|
app: app,
|
activationPolicy: activationPolicy(from: runningApplication),
|
title: title(rawWindow: rawWindow, axWindow: axWindow),
|
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
|
),
|
isDesktopElement: isDesktopElement(rawWindow: rawWindow, app: app),
|
isSystemCritical: isSystemCritical(rawWindow: rawWindow, app: app),
|
isInteractable: axWindow?.isInteractable ?? true,
|
hasAXBacking: axWindow != nil,
|
isOnscreen: boolValue(rawWindow[kCGWindowIsOnscreen as String]),
|
spaceIDs: spaceIDs
|
)
|
|
if axWindow?.isMinimized == true, let axWindow {
|
nextRestorableAXWindowsByID[windowID] = RestorableAXWindow(
|
processIdentifier: axWindow.processIdentifier,
|
element: axWindow.element
|
)
|
}
|
if let axWindow, !record.isGhost {
|
consumedAXMetadata.insert(AXWindowIdentity(axWindow))
|
}
|
|
return record
|
}
|
|
var axOnlyRecords: [WindowEnumerationRecord] = []
|
for axWindow in axMetadata.values where axWindow.isMinimized {
|
guard let windowID = axWindow.windowID else { continue }
|
nextRestorableAXWindowsByID[windowID] = RestorableAXWindow(
|
processIdentifier: axWindow.processIdentifier,
|
element: axWindow.element
|
)
|
}
|
|
for axWindow in axWindows {
|
guard !consumedAXMetadata.contains(AXWindowIdentity(axWindow)) else { continue }
|
guard axWindow.subrole.isExplicitlySupported else { continue }
|
guard axWindow.hasExplicitSubrole else { continue }
|
|
let resolvedWindowID: UInt32
|
let identifierSource: AlignerWindow.IdentifierSource
|
if let windowID = axWindow.windowID {
|
guard !cgWindowIDs.contains(windowID) else { continue }
|
resolvedWindowID = windowID
|
identifierSource = .cgWindow
|
} else if shouldCreateSyntheticAXFallback(for: axWindow) {
|
guard !cgWindowFingerprints.contains(AXWindowFingerprint(axWindow)) else { continue }
|
|
resolvedWindowID = MinimizedWindowFallbackPolicy.syntheticWindowID(
|
processIdentifier: axWindow.processIdentifier,
|
windowIndex: axWindow.windowIndex,
|
title: axWindow.title ?? "",
|
subroleDescription: axWindow.subrole.fallbackDescription,
|
occupiedWindowIDs: occupiedWindowIDs
|
)
|
occupiedWindowIDs.insert(resolvedWindowID)
|
identifierSource = .syntheticAX
|
} else {
|
continue
|
}
|
|
let spaceIDs = spaceIDsByWindowID[resolvedWindowID] ?? []
|
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,
|
activationPolicy: axWindow.activationPolicy,
|
title: axWindow.title ?? "",
|
identifierSource: identifierSource,
|
size: axWindow.size,
|
frame: axWindow.frame,
|
subrole: axWindow.subrole,
|
isMinimized: axWindow.isMinimized,
|
isFullscreen: isFullscreen,
|
isGhost: false,
|
isDesktopElement: false,
|
isSystemCritical: isSystemCritical(app: axWindow.app),
|
isInteractable: axWindow.isInteractable,
|
hasAXBacking: true,
|
isOnscreen: false,
|
spaceIDs: spaceIDs
|
))
|
}
|
|
restorableAXWindowsByID = nextRestorableAXWindowsByID
|
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(
|
from runningApplication: NSRunningApplication?,
|
rawWindow: [String: Any],
|
ownerPID: Int32?
|
) -> AlignerApp {
|
let bundleIdentifier = runningApplication?.bundleIdentifier ?? ""
|
let name = runningApplication?.localizedName
|
?? stringValue(rawWindow[kCGWindowOwnerName as String])
|
?? "Unknown"
|
let provisionalApp = AlignerApp(
|
bundleIdentifier: bundleIdentifier,
|
name: name,
|
category: .generic,
|
isHidden: runningApplication?.isHidden ?? false,
|
executablePath: runningApplication?.bundleURL?.path,
|
processIdentifier: ownerPID
|
)
|
|
return AlignerApp(
|
bundleIdentifier: provisionalApp.bundleIdentifier,
|
name: provisionalApp.name,
|
category: appCategorizer.category(for: provisionalApp),
|
isHidden: provisionalApp.isHidden,
|
executablePath: provisionalApp.executablePath,
|
processIdentifier: provisionalApp.processIdentifier
|
)
|
}
|
|
private func activateApplication(for window: AlignerWindow) -> Bool {
|
guard
|
let processIdentifier = window.app.processIdentifier
|
else {
|
return false
|
}
|
|
return activateApplication(processIdentifier: processIdentifier)
|
}
|
|
private func activateApplication(processIdentifier: Int32) -> Bool {
|
guard let application = NSRunningApplication(processIdentifier: processIdentifier) else {
|
DevelopmentDiagnostics.log("windowActivation.activateApplication.missingApplication", [
|
"pid": processIdentifier
|
])
|
return false
|
}
|
|
let activated = application.activate(options: [.activateAllWindows])
|
DevelopmentDiagnostics.log("windowActivation.activateApplication", [
|
"pid": processIdentifier,
|
"localizedName": application.localizedName,
|
"bundleIdentifier": application.bundleIdentifier,
|
"activationPolicy": application.activationPolicy.rawValue,
|
"activateOptions": "activateAllWindows",
|
"isActive": application.isActive,
|
"result": activated
|
])
|
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(
|
appElement,
|
kAXFocusedWindowAttribute as CFString,
|
window
|
)
|
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 }
|
|
switch runningApplication.activationPolicy {
|
case .regular:
|
return .regular
|
case .accessory:
|
return .accessory
|
case .prohibited:
|
return .prohibited
|
@unknown default:
|
return .unknown
|
}
|
}
|
|
private func title(rawWindow: [String: Any], axWindow: AXWindowMetadata?) -> String {
|
if let axTitle = axWindow?.title, !axTitle.isEmpty {
|
return axTitle
|
}
|
|
return stringValue(rawWindow[kCGWindowName as String]) ?? ""
|
}
|
|
private func isGhost(
|
rawWindow: [String: Any],
|
axWindow: AXWindowMetadata?,
|
app: AlignerApp,
|
spaceIDs: [UInt64],
|
hasAXBackedTitleDuplicate: Bool = false
|
) -> 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 rawTitle = stringValue(rawWindow[kCGWindowName as String]) ?? ""
|
let rawBounds = bounds(rawWindow[kCGWindowBounds as String])
|
|
if axWindow?.isMinimized == true {
|
return false
|
}
|
if axWindow != nil {
|
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
|
}
|
if rawTitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, alpha < 0.99 {
|
return true
|
}
|
if !spaceIDs.isEmpty {
|
return layer != 0 || alpha <= 0
|
}
|
|
return layer != 0 || alpha <= 0 || !isOnscreen
|
}
|
|
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 {
|
guard app.bundleIdentifier == "com.apple.finder" else { return false }
|
let title = stringValue(rawWindow[kCGWindowName as String]) ?? ""
|
let ownerName = stringValue(rawWindow[kCGWindowOwnerName as String]) ?? ""
|
return title == "Desktop" || ownerName == "Dock"
|
}
|
|
private func isSystemCritical(rawWindow: [String: Any], app: AlignerApp) -> Bool {
|
let title = stringValue(rawWindow[kCGWindowName as String]) ?? ""
|
return SystemCriticalWindowClassifier.isSystemCritical(app: app)
|
|| SystemCriticalWindowClassifier.isSystemCriticalTitle(title)
|
}
|
|
private func isSystemCritical(app: AlignerApp) -> Bool {
|
SystemCriticalWindowClassifier.isSystemCritical(app: app)
|
}
|
|
private func shouldCreateSyntheticAXFallback(for axWindow: AXWindowMetadata) -> Bool {
|
MinimizedWindowFallbackPolicy.shouldCreateAXOnlyFallback(
|
hasCGWindowID: axWindow.windowID != nil,
|
isMinimized: axWindow.isMinimized
|
)
|
}
|
|
private static func restoreResultString(_ result: WindowRestoreResult) -> String {
|
switch result {
|
case .restored:
|
return "restored"
|
case .windowNotFound:
|
return "windowNotFound"
|
case .unsupported:
|
return "unsupported"
|
case .activationFailed:
|
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 {
|
let windowID: UInt32?
|
let app: AlignerApp
|
let activationPolicy: AlignerActivationPolicy
|
let title: String?
|
let isMinimized: Bool
|
let size: CGSize
|
let frame: CGRect?
|
let subrole: AlignerWindowSubrole
|
let hasExplicitSubrole: Bool
|
let isInteractable: Bool
|
let processIdentifier: Int32
|
let windowIndex: Int
|
let element: AXUIElement
|
}
|
|
private struct AXWindowIdentity: Hashable {
|
let processIdentifier: Int32
|
let windowID: UInt32?
|
let windowIndex: Int
|
|
init(_ metadata: AXWindowMetadata) {
|
processIdentifier = metadata.processIdentifier
|
windowID = metadata.windowID
|
windowIndex = metadata.windowIndex
|
}
|
}
|
|
private struct AXWindowTitleKey: Hashable {
|
let processIdentifier: Int32
|
let title: String
|
|
init?(processIdentifier: Int32, title: String?) {
|
let normalizedTitle = title?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
guard !normalizedTitle.isEmpty else { return nil }
|
|
self.processIdentifier = processIdentifier
|
self.title = normalizedTitle
|
}
|
|
init?(_ metadata: AXWindowMetadata) {
|
self.init(processIdentifier: metadata.processIdentifier, title: metadata.title)
|
}
|
}
|
|
private struct AXWindowFingerprint: Hashable {
|
let processIdentifier: Int32
|
let title: String
|
let width: Int
|
let height: Int
|
|
init(processIdentifier: Int32, title: String, size: CGSize) {
|
self.processIdentifier = processIdentifier
|
self.title = title
|
self.width = Int(size.width.rounded())
|
self.height = Int(size.height.rounded())
|
}
|
|
init(_ metadata: AXWindowMetadata) {
|
self.init(
|
processIdentifier: metadata.processIdentifier,
|
title: metadata.title ?? "",
|
size: metadata.size
|
)
|
}
|
}
|
|
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
|
}
|
|
private struct AXActionResult {
|
let error: AXError
|
|
var success: Bool {
|
error == .success
|
}
|
}
|
|
private enum AXWindowRestoreError: Error {
|
case setMinimizedFailed(AXError)
|
case raiseFailed(AXError)
|
}
|
|
private enum AXWindowMetadataReader {
|
static func metadata(appCategorizer: any AppCategorizer) -> [AXWindowMetadata] {
|
var result: [AXWindowMetadata] = []
|
|
for application in NSWorkspace.shared.runningApplications {
|
guard application.activationPolicy == .regular else { continue }
|
let app = app(from: application, appCategorizer: appCategorizer)
|
|
let appElement = AXUIElementCreateApplication(application.processIdentifier)
|
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,
|
frame: position.map { CGRect(origin: $0, size: size) },
|
subrole: subrole.value,
|
hasExplicitSubrole: subrole.isExplicit,
|
isInteractable: boolAttribute(kAXEnabledAttribute as String, for: window) ?? true,
|
processIdentifier: application.processIdentifier,
|
windowIndex: windowIndex,
|
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
|
}
|
|
private static func app(
|
from application: NSRunningApplication,
|
appCategorizer: any AppCategorizer
|
) -> AlignerApp {
|
let provisionalApp = AlignerApp(
|
bundleIdentifier: application.bundleIdentifier ?? "",
|
name: application.localizedName ?? "Unknown",
|
category: .generic,
|
isHidden: application.isHidden,
|
executablePath: application.bundleURL?.path,
|
processIdentifier: application.processIdentifier
|
)
|
|
return AlignerApp(
|
bundleIdentifier: provisionalApp.bundleIdentifier,
|
name: provisionalApp.name,
|
category: appCategorizer.category(for: provisionalApp),
|
isHidden: provisionalApp.isHidden,
|
executablePath: provisionalApp.executablePath,
|
processIdentifier: provisionalApp.processIdentifier
|
)
|
}
|
|
private static func activationPolicy(from application: NSRunningApplication) -> AlignerActivationPolicy {
|
switch application.activationPolicy {
|
case .regular:
|
return .regular
|
case .accessory:
|
return .accessory
|
case .prohibited:
|
return .prohibited
|
@unknown default:
|
return .unknown
|
}
|
}
|
|
private static func windowID(for window: AXUIElement) -> UInt32? {
|
var value: CFTypeRef?
|
if AXUIElementCopyAttributeValue(window, "AXWindowNumber" as CFString, &value) == .success,
|
let windowID = uint32Value(value) {
|
return windowID
|
}
|
|
return PrivateAXWindowIDReader.windowID(for: window)
|
}
|
|
private static func title(for window: AXUIElement) -> String? {
|
var value: CFTypeRef?
|
guard AXUIElementCopyAttributeValue(window, kAXTitleAttribute as CFString, &value) == .success else {
|
return nil
|
}
|
|
return value as? String
|
}
|
|
private static func boolAttribute(_ attribute: String, for window: AXUIElement) -> Bool? {
|
var value: CFTypeRef?
|
guard AXUIElementCopyAttributeValue(window, attribute as CFString, &value) == .success else {
|
return nil
|
}
|
|
return boolValue(value)
|
}
|
|
private static func size(for window: AXUIElement) -> CGSize {
|
var value: CFTypeRef?
|
guard AXUIElementCopyAttributeValue(window, kAXSizeAttribute as CFString, &value) == .success,
|
let axValue = value,
|
CFGetTypeID(axValue) == AXValueGetTypeID()
|
else {
|
return .zero
|
}
|
|
var size = CGSize.zero
|
guard AXValueGetValue(axValue as! AXValue, .cgSize, &size) else {
|
return .zero
|
}
|
|
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,
|
let subrole = value as? String
|
else {
|
return (.standard, false)
|
}
|
|
switch subrole {
|
case "AXStandardWindow":
|
return (.standard, true)
|
case "AXDialog", "AXModalDialog":
|
return (.dialog, true)
|
case "AXSheet":
|
return (.sheet, true)
|
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)
|
}
|
}
|
|
private extension AlignerWindowSubrole {
|
var isExplicitlySupported: Bool {
|
switch self {
|
case .standard, .dialog, .sheet:
|
return true
|
case .unknown:
|
return false
|
}
|
}
|
|
var fallbackDescription: String {
|
switch self {
|
case .standard:
|
return "standard"
|
case .dialog:
|
return "dialog"
|
case .sheet:
|
return "sheet"
|
case .unknown(let rawValue):
|
return rawValue
|
}
|
}
|
}
|
|
private func bounds(_ value: Any?) -> CGRect {
|
guard let dictionary = value as? NSDictionary else {
|
return .zero
|
}
|
|
var rect = CGRect.zero
|
CGRectMakeWithDictionaryRepresentation(dictionary as CFDictionary, &rect)
|
return rect
|
}
|
|
private func stringValue(_ value: Any?) -> String? {
|
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:
|
return bool
|
case let number as NSNumber:
|
return number.boolValue
|
default:
|
return false
|
}
|
}
|
|
private func intValue(_ value: Any?) -> Int? {
|
switch value {
|
case let number as NSNumber:
|
return number.intValue
|
case let int as Int:
|
return int
|
default:
|
return nil
|
}
|
}
|
|
private func int32Value(_ value: Any?) -> Int32? {
|
switch value {
|
case let number as NSNumber:
|
return number.int32Value
|
case let int32 as Int32:
|
return int32
|
case let int as Int:
|
return Int32(int)
|
default:
|
return nil
|
}
|
}
|
|
private func uint32Value(_ value: Any?) -> UInt32? {
|
switch value {
|
case let number as NSNumber:
|
return number.uint32Value
|
case let uint32 as UInt32:
|
return uint32
|
case let int as Int where int >= 0:
|
return UInt32(int)
|
default:
|
return nil
|
}
|
}
|
|
private func doubleValue(_ value: Any?) -> Double? {
|
switch value {
|
case let number as NSNumber:
|
return number.doubleValue
|
case let double as Double:
|
return double
|
default:
|
return nil
|
}
|
}
|