From 6983a4ed2137bf1a8377e5d1cf82183e02e617f8 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sat, 13 Jun 2026 14:08:17 +0800
Subject: [PATCH] Fix multi-page targeting and split view tile QA
---
C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift | 619 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 files changed, 597 insertions(+), 22 deletions(-)
diff --git a/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift b/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
index 773abed..87b6c01 100644
--- a/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
+++ b/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
@@ -1,10 +1,10 @@
import AppKit
-import ApplicationServices
+@preconcurrency import ApplicationServices
import CoreGraphics
import Foundation
import AlignerCore
-final class CGWindowAXWindowService: WindowServiceProtocol, WindowRestorationServiceProtocol, WindowActivationServiceProtocol {
+final class CGWindowAXWindowService: WindowServiceProtocol, WindowRestorationServiceProtocol, WindowActivationServiceProtocol, WindowCloseServiceProtocol {
weak var delegate: WindowServiceDelegate?
private let appCategorizer: any AppCategorizer
@@ -162,16 +162,12 @@
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,
@@ -255,6 +251,420 @@
return didPrivatelyActivate || focusedBeforeActivate || focusedAfterActivate || raisedBeforeActivate || raisedAfterActivate
? .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
+ }
+
+ 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 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 = 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(
+ for window: AlignerWindow,
+ in axMetadata: [AXWindowMetadata]
+ ) -> Int {
+ guard let processIdentifier = window.app.processIdentifier else { return 0 }
+ let normalizedTitle = normalizedWindowTitle(window.title)
+ return axMetadata.filter { metadata in
+ metadata.processIdentifier == processIdentifier
+ && normalizedWindowTitle(metadata.title ?? "") == normalizedTitle
+ }.count
+ }
+
+ private func matchingAXSizeCandidateCount(
+ for window: AlignerWindow,
+ in axMetadata: [AXWindowMetadata]
+ ) -> Int {
+ guard let processIdentifier = window.app.processIdentifier,
+ let cgFingerprint = cgWindowFingerprint(for: window)
+ else {
+ return 0
+ }
+
+ return axMetadata.filter { metadata in
+ metadata.processIdentifier == processIdentifier
+ && Int(metadata.size.width.rounded()) == cgFingerprint.width
+ && Int(metadata.size.height.rounded()) == cgFingerprint.height
+ }.count
+ }
+
+ func close(app: AlignerApp) throws -> WindowCloseResult {
+ DevelopmentDiagnostics.log("windowClose.app.start", [
+ "appName": app.name,
+ "bundleIdentifier": app.bundleIdentifier,
+ "pid": app.processIdentifier
+ ])
+
+ guard let processIdentifier = app.processIdentifier else {
+ DevelopmentDiagnostics.log("windowClose.app.missingPID", [
+ "appName": app.name,
+ "bundleIdentifier": app.bundleIdentifier
+ ])
+ return .appNotFound
+ }
+ guard let runningApplication = NSRunningApplication(processIdentifier: processIdentifier) else {
+ DevelopmentDiagnostics.log("windowClose.app.missingApplication", [
+ "appName": app.name,
+ "pid": processIdentifier
+ ])
+ return .appNotFound
+ }
+
+ let requested = runningApplication.terminate()
+ DevelopmentDiagnostics.log("windowClose.app.result", [
+ "appName": app.name,
+ "pid": processIdentifier,
+ "requested": requested
+ ])
+ return requested ? .requested : .failed("terminateFailed")
}
private func activateViaPrivateWindowServerAPI(_ window: AlignerWindow) -> PrivateWindowActivationOutcome? {
@@ -362,13 +772,14 @@
return nil
}
+ let rawBounds = bounds(rawWindow[kCGWindowBounds as String])
let ownerPID = int32Value(rawWindow[kCGWindowOwnerPID as String])
let runningApplication = ownerPID.flatMap(NSRunningApplication.init(processIdentifier:))
let rawFingerprint = ownerPID.map {
AXWindowFingerprint(
processIdentifier: $0,
title: title(rawWindow: rawWindow, axWindow: nil),
- size: bounds(rawWindow[kCGWindowBounds as String]).size
+ size: rawBounds.size
)
}
let directAXWindow = axMetadata[windowID]
@@ -388,13 +799,15 @@
app: app,
activationPolicy: activationPolicy(from: runningApplication),
title: title(rawWindow: rawWindow, axWindow: axWindow),
- size: bounds(rawWindow[kCGWindowBounds as String]).size,
+ size: rawBounds.size,
+ frame: rawBounds,
subrole: axWindow?.subrole ?? .standard,
isMinimized: axWindow?.isMinimized ?? false,
isFullscreen: !Set(spaceIDs).isDisjoint(with: fullscreenSpaceIDs),
isGhost: isGhost(
rawWindow: rawWindow,
axWindow: axWindow,
+ app: app,
spaceIDs: spaceIDs,
hasAXBackedTitleDuplicate: hasAXBackedTitleDuplicate
),
@@ -467,6 +880,7 @@
title: axWindow.title ?? "",
identifierSource: identifierSource,
size: axWindow.size,
+ frame: axWindow.frame,
subrole: axWindow.subrole,
isMinimized: axWindow.isMinimized,
isFullscreen: !Set(spaceIDs).isDisjoint(with: fullscreenSpaceIDs),
@@ -583,6 +997,7 @@
private func isGhost(
rawWindow: [String: Any],
axWindow: AXWindowMetadata?,
+ app: AlignerApp,
spaceIDs: [UInt64],
hasAXBackedTitleDuplicate: Bool = false
) -> Bool {
@@ -596,10 +1011,20 @@
return false
}
if axWindow != nil {
- return layer != 0 || alpha <= 0
+ if alpha <= 0 {
+ return true
+ }
+ if layer == 0 {
+ return false
+ }
+
+ return !isOnscreen
}
if hasAXBackedTitleDuplicate {
return true
+ }
+ if isKnownCGOnlyMainWindow(rawWindow: rawWindow, app: app) {
+ return false
}
if !spaceIDs.isEmpty, isLikelyFullscreenAuxiliaryCGWindow(bounds: rawBounds) {
return true
@@ -616,6 +1041,40 @@
private func isLikelyFullscreenAuxiliaryCGWindow(bounds: CGRect) -> Bool {
bounds.width < 500 || bounds.height < 160
+ }
+
+ private func isKnownCGOnlyMainWindow(rawWindow: [String: Any], app: AlignerApp) -> Bool {
+ let layer = intValue(rawWindow[kCGWindowLayer as String]) ?? 0
+ let alpha = doubleValue(rawWindow[kCGWindowAlpha as String]) ?? 1
+ let isOnscreen = boolValue(rawWindow[kCGWindowIsOnscreen as String])
+ let title = stringValue(rawWindow[kCGWindowName as String]) ?? ""
+ let rawBounds = bounds(rawWindow[kCGWindowBounds as String])
+ let trimmedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
+
+ if app.bundleIdentifier == "cc.ffitch.shottr" {
+ return isOnscreen
+ && alpha > 0
+ && trimmedTitle == "Shottr"
+ && rawBounds.width > WindowEnumerationPolicy.minimumVisibleWidth
+ && rawBounds.height > WindowEnumerationPolicy.minimumVisibleHeight
+ }
+
+ if app.bundleIdentifier == "com.apple.finder" {
+ return layer == 0
+ && alpha > 0
+ && !trimmedTitle.isEmpty
+ && rawBounds.width > WindowEnumerationPolicy.minimumVisibleWidth
+ && rawBounds.height > WindowEnumerationPolicy.minimumVisibleHeight
+ }
+
+ guard app.bundleIdentifier == "io.dcloud.HBuilderX" else { return false }
+
+ return layer == 0
+ && alpha > 0
+ && !isOnscreen
+ && !trimmedTitle.isEmpty
+ && rawBounds.width > WindowEnumerationPolicy.minimumVisibleWidth
+ && rawBounds.height > WindowEnumerationPolicy.minimumVisibleHeight
}
private func isDesktopElement(rawWindow: [String: Any], app: AlignerApp) -> Bool {
@@ -654,6 +1113,8 @@
return "activationFailed"
}
}
+
+ private static let commandWVirtualKeyCode: CGKeyCode = 13
}
private struct AXWindowMetadata {
@@ -663,6 +1124,7 @@
let title: String?
let isMinimized: Bool
let size: CGSize
+ let frame: CGRect?
let subrole: AlignerWindowSubrole
let hasExplicitSubrole: Bool
let isInteractable: Bool
@@ -722,6 +1184,34 @@
}
}
+private struct AXWindowGeometryFingerprint: Hashable {
+ let processIdentifier: Int32
+ let title: String
+ let x: Int
+ let y: Int
+ let width: Int
+ let height: Int
+
+ init?(processIdentifier: Int32, title: String, frame: CGRect?) {
+ guard let frame else { return nil }
+
+ self.processIdentifier = processIdentifier
+ self.title = title
+ self.x = Int(frame.origin.x.rounded())
+ self.y = Int(frame.origin.y.rounded())
+ self.width = Int(frame.size.width.rounded())
+ self.height = Int(frame.size.height.rounded())
+ }
+
+ init?(_ metadata: AXWindowMetadata) {
+ self.init(
+ processIdentifier: metadata.processIdentifier,
+ title: normalizedWindowTitle(metadata.title ?? ""),
+ frame: metadata.frame
+ )
+ }
+}
+
private struct RestorableAXWindow {
let processIdentifier: Int32
let element: AXUIElement
@@ -749,22 +1239,21 @@
let app = app(from: application, appCategorizer: appCategorizer)
let appElement = AXUIElementCreateApplication(application.processIdentifier)
- var windowsValue: CFTypeRef?
- guard AXUIElementCopyAttributeValue(appElement, kAXWindowsAttribute as CFString, &windowsValue) == .success,
- let windows = windowsValue as? [AXUIElement]
- else {
- continue
- }
+ let windows = windows(for: appElement)
+ guard !windows.isEmpty else { continue }
for (windowIndex, window) in windows.enumerated() {
let subrole = subrole(for: window)
+ let size = size(for: window)
+ let position = position(for: window)
result.append(AXWindowMetadata(
windowID: windowID(for: window),
app: app,
activationPolicy: activationPolicy(from: application),
title: title(for: window),
isMinimized: boolAttribute(kAXMinimizedAttribute as String, for: window) ?? false,
- size: size(for: window),
+ size: size,
+ frame: position.map { CGRect(origin: $0, size: size) },
subrole: subrole.value,
hasExplicitSubrole: subrole.isExplicit,
isInteractable: boolAttribute(kAXEnabledAttribute as String, for: window) ?? true,
@@ -773,6 +1262,38 @@
element: window
))
}
+ }
+
+ return result
+ }
+
+ private static func windows(for appElement: AXUIElement) -> [AXUIElement] {
+ var result: [AXUIElement] = []
+ var seen = Set<AXWindowElementKey>()
+
+ func append(_ window: AXUIElement) {
+ let key = AXWindowElementKey(window)
+ guard !seen.contains(key) else { return }
+ seen.insert(key)
+ result.append(window)
+ }
+
+ var windowsValue: CFTypeRef?
+ if AXUIElementCopyAttributeValue(appElement, kAXWindowsAttribute as CFString, &windowsValue) == .success,
+ let windows = windowsValue as? [AXUIElement] {
+ windows.forEach(append)
+ }
+
+ for attribute in [kAXFocusedWindowAttribute, kAXMainWindowAttribute] {
+ var windowValue: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(appElement, attribute as CFString, &windowValue) == .success,
+ let window = windowValue,
+ CFGetTypeID(window) == AXUIElementGetTypeID()
+ else {
+ continue
+ }
+
+ append(window as! AXUIElement)
}
return result
@@ -859,6 +1380,23 @@
return size
}
+ private static func position(for window: AXUIElement) -> CGPoint? {
+ var value: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(window, kAXPositionAttribute as CFString, &value) == .success,
+ let axValue = value,
+ CFGetTypeID(axValue) == AXValueGetTypeID()
+ else {
+ return nil
+ }
+
+ var position = CGPoint.zero
+ guard AXValueGetValue(axValue as! AXValue, .cgPoint, &position) else {
+ return nil
+ }
+
+ return position
+ }
+
private static func subrole(for window: AXUIElement) -> (value: AlignerWindowSubrole, isExplicit: Bool) {
var value: CFTypeRef?
guard AXUIElementCopyAttributeValue(window, kAXSubroleAttribute as CFString, &value) == .success,
@@ -877,6 +1415,39 @@
default:
return (.unknown(subrole), true)
}
+ }
+}
+
+private struct AXWindowElementKey: Hashable {
+ private let typeID: CFTypeID
+ private let pointer: UInt
+ private let windowID: UInt32?
+
+ init(_ element: AXUIElement) {
+ self.typeID = CFGetTypeID(element)
+ self.pointer = UInt(bitPattern: Unmanaged.passUnretained(element).toOpaque())
+ self.windowID = PrivateAXWindowIDReader.windowID(for: element)
+ }
+
+ static func == (lhs: AXWindowElementKey, rhs: AXWindowElementKey) -> Bool {
+ if let lhsWindowID = lhs.windowID,
+ let rhsWindowID = rhs.windowID {
+ return lhsWindowID == rhsWindowID
+ }
+
+ return lhs.typeID == rhs.typeID && lhs.pointer == rhs.pointer
+ }
+
+ func hash(into hasher: inout Hasher) {
+ if let windowID {
+ hasher.combine("windowID")
+ hasher.combine(windowID)
+ return
+ }
+
+ hasher.combine("pointer")
+ hasher.combine(typeID)
+ hasher.combine(pointer)
}
}
@@ -918,6 +1489,10 @@
value as? String
}
+private func normalizedWindowTitle(_ title: String) -> String {
+ title.trimmingCharacters(in: .whitespacesAndNewlines)
+}
+
private func boolValue(_ value: Any?) -> Bool {
switch value {
case let bool as Bool:
--
Gitblit v1.9.3