| | |
| | | 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 axWindow = axWindow( |
| | | for: window, |
| | | in: AXWindowMetadataReader.metadata(appCategorizer: appCategorizer), |
| | | operation: "windowActivation", |
| | | matchAttempt: "initial" |
| | | ) |
| | | |
| | | DevelopmentDiagnostics.log("windowActivation.activate.axLookup", [ |
| | | "windowID": window.id, |
| | |
| | | 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) |
| | |
| | | "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 { |
| | |
| | | } |
| | | } |
| | | |
| | | 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 |
| | | } |
| | | |
| | | 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("windowClose.axMatch.directWindowID", [ |
| | | DevelopmentDiagnostics.log("\(operation).axMatch.directWindowID", [ |
| | | "windowID": window.id, |
| | | "pid": window.app.processIdentifier, |
| | | "attempt": matchAttempt |
| | |
| | | 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("windowClose.axMatch.fingerprint", [ |
| | | DevelopmentDiagnostics.log("\(operation).axMatch.fingerprint", [ |
| | | "windowID": window.id, |
| | | "pid": processIdentifier, |
| | | "attempt": matchAttempt |
| | |
| | | } |
| | | |
| | | if fingerprintMatches.count > 1 { |
| | | DevelopmentDiagnostics.log("windowClose.axMatch.fingerprintAmbiguous", [ |
| | | DevelopmentDiagnostics.log("\(operation).axMatch.fingerprintAmbiguous", [ |
| | | "windowID": window.id, |
| | | "pid": processIdentifier, |
| | | "candidateCount": fingerprintMatches.count |
| | | "candidateCount": fingerprintMatches.count, |
| | | "attempt": matchAttempt |
| | | ]) |
| | | } |
| | | } |
| | |
| | | metadata.processIdentifier == processIdentifier |
| | | && normalizedWindowTitle(metadata.title ?? "") == normalizedTitle |
| | | } |
| | | if titleMatches.count == 1 { |
| | | DevelopmentDiagnostics.log("windowClose.axMatch.normalizedTitle", [ |
| | | if !normalizedTitle.isEmpty, titleMatches.count == 1 { |
| | | DevelopmentDiagnostics.log("\(operation).axMatch.normalizedTitle", [ |
| | | "windowID": window.id, |
| | | "pid": processIdentifier, |
| | | "attempt": matchAttempt |
| | |
| | | processIdentifier: processIdentifier, |
| | | in: axMetadata |
| | | ) { |
| | | DevelopmentDiagnostics.log("windowClose.axMatch.finderFallback", [ |
| | | DevelopmentDiagnostics.log("\(operation).axMatch.finderFallback", [ |
| | | "windowID": window.id, |
| | | "pid": processIdentifier, |
| | | "attempt": matchAttempt |
| | |
| | | } |
| | | |
| | | if titleMatches.count > 1 { |
| | | DevelopmentDiagnostics.log("windowClose.axMatch.titleAmbiguous", [ |
| | | DevelopmentDiagnostics.log("\(operation).axMatch.titleAmbiguous", [ |
| | | "windowID": window.id, |
| | | "pid": processIdentifier, |
| | | "candidateCount": titleMatches.count |
| | | "candidateCount": titleMatches.count, |
| | | "attempt": matchAttempt |
| | | ]) |
| | | } |
| | | |
| | |
| | | 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 = cgWindowFrame(for: window) ?? window.frame |
| | | 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 matchingAXTitleCandidateCount( |
| | |
| | | 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] |
| | |
| | | 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), |
| | |
| | | title: axWindow.title ?? "", |
| | | identifierSource: identifierSource, |
| | | size: axWindow.size, |
| | | frame: axWindow.frame, |
| | | subrole: axWindow.subrole, |
| | | isMinimized: axWindow.isMinimized, |
| | | isFullscreen: !Set(spaceIDs).isDisjoint(with: fullscreenSpaceIDs), |
| | |
| | | } |
| | | } |
| | | |
| | | private static let commandWVirtualKeyCode: CGKeyCode = 13 |
| | | } |
| | | |
| | | private struct AXWindowMetadata { |
| | |
| | | let title: String? |
| | | let isMinimized: Bool |
| | | let size: CGSize |
| | | let frame: CGRect? |
| | | let subrole: AlignerWindowSubrole |
| | | let hasExplicitSubrole: Bool |
| | | let isInteractable: Bool |
| | |
| | | } |
| | | } |
| | | |
| | | 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 |
| | |
| | | |
| | | 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, |
| | |
| | | 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, |