@@ -106,6 +106,18 @@ func requireNonSecure(_ element: AXUIElement) throws {
106106func rectJSON( _ rect: CGRect ) -> [ String : Double ] {
107107 [ " x " : rect. minX, " y " : rect. minY, " width " : rect. width, " height " : rect. height]
108108}
109+ /// Whether keyboard focus is on `element` or an editor inside it, such as a combo box's field.
110+ func holdsFocus( _ focused: CFTypeRef , _ element: AXUIElement ) -> Bool {
111+ guard CFGetTypeID ( focused) == AXUIElementGetTypeID ( ) else { return false }
112+ var current : AXUIElement ? = unsafeBitCast ( focused, to: AXUIElement . self)
113+ for _ in 0 ..< 8 {
114+ guard let candidate = current else { return false }
115+ if CFEqual ( candidate, element) { return true }
116+ guard let parent = attribute ( candidate, kAXParentAttribute) , CFGetTypeID ( parent) == AXUIElementGetTypeID ( ) else { return false }
117+ current = unsafeBitCast ( parent, to: AXUIElement . self)
118+ }
119+ return false
120+ }
109121func axActions( _ element: AXUIElement ) -> [ String ] {
110122 var names : CFArray ?
111123 return AXUIElementCopyActionNames ( element, & names) == . success ? names as? [ String ] ?? [ ] : [ ]
@@ -656,7 +668,8 @@ final class Driver {
656668 } else {
657669 try await coordinateAction { dispatch in
658670 let position = try point ( p, app: app, snapshot: snapshot) ; let mouse : CGMouseButton = button == " right " ? . right : . left
659- let windowID = try required ( p. windowId ?? snapshot. windowFrames. first ( where: { $0. value. contains ( position) } ) ? . key, " windowId " )
671+ // Coordinates are window-local to the requested (or selected) window; an element target uses its own.
672+ let windowID = try required ( p. windowId ?? ( p. elementId == nil ? snapshot. selectedWindowId : snapshot. windowFrames. first ( where: { $0. value. contains ( position) } ) ? . key) , " windowId " )
660673 let frame = try required ( snapshot. windowFrames [ windowID] , " window frame " )
661674 for click in 1 ... count {
662675 try checkCancellation ( )
@@ -683,18 +696,6 @@ final class Driver {
683696 try axCheck ( AXUIElementSetAttributeValue ( element, kAXFocusedAttribute as CFString , kCFBooleanTrue) )
684697 try await Task . sleep ( for: . milliseconds( 50 ) )
685698 }
686- /// Focus may land on the element itself or on an editor inside it, such as a combo box's field.
687- func holdsFocus( _ focused: CFTypeRef ) -> Bool {
688- guard CFGetTypeID ( focused) == AXUIElementGetTypeID ( ) else { return false }
689- var current : AXUIElement ? = unsafeBitCast ( focused, to: AXUIElement . self)
690- for _ in 0 ..< 8 {
691- guard let candidate = current else { return false }
692- if CFEqual ( candidate, element) { return true }
693- guard let parent = attribute ( candidate, kAXParentAttribute) , CFGetTypeID ( parent) == AXUIElementGetTypeID ( ) else { return false }
694- current = unsafeBitCast ( parent, to: AXUIElement . self)
695- }
696- return false
697- }
698699 let observedWindows = snapshot. observedWindows ( )
699700 func validateFocus( ) throws {
700701 try checkCancellation ( ) ; try requireNonSecure ( element)
@@ -703,7 +704,7 @@ final class Driver {
703704 acceptsInput ( unsafeBitCast ( rawWindow, to: AXUIElement . self) , observed: observedWindows) else {
704705 throw ComputerError ( " stale_window " , " The editor's window moved or is not an observed window; observe again. " )
705706 }
706- guard let current = attribute ( root, kAXFocusedUIElementAttribute) , holdsFocus ( current) else { throw ComputerError ( " focus_changed " , " The exact editor is no longer focused; remaining input was not dispatched. Observe again. " ) }
707+ guard let current = attribute ( root, kAXFocusedUIElementAttribute) , holdsFocus ( current, element ) else { throw ComputerError ( " focus_changed " , " The exact editor is no longer focused; remaining input was not dispatched. Observe again. " ) }
707708 if let focused = attribute ( root, kAXFocusedUIElementAttribute) , CFGetTypeID ( focused) == AXUIElementGetTypeID ( ) { try requireNonSecure ( unsafeBitCast ( focused, to: AXUIElement . self) ) }
708709 }
709710 // AX focus requests and app activation may settle asynchronously. Wait without
@@ -756,10 +757,17 @@ final class Driver {
756757 // A targeted key first moves keyboard focus to that element, as clicking into it would.
757758 if let elementId = p. elementId {
758759 let element = try snapshot. element ( elementId)
759- if attribute ( root, kAXFocusedUIElementAttribute) . map ( { CFEqual ( $0, element) } ) != true {
760+ if attribute ( root, kAXFocusedUIElementAttribute) . map ( { holdsFocus ( $0, element) } ) != true {
760761 try axCheck ( AXUIElementSetAttributeValue ( element, kAXFocusedAttribute as CFString , kCFBooleanTrue) )
762+ }
763+ // Focus can settle asynchronously; the key is sent only once this element holds it.
764+ var focused = false
765+ for _ in 0 ..< 10 {
766+ try checkCancellation ( )
767+ if attribute ( root, kAXFocusedUIElementAttribute) . map ( { holdsFocus ( $0, element) } ) == true { focused = true ; break }
761768 try await Task . sleep ( for: . milliseconds( 50 ) )
762769 }
770+ guard focused else { throw ComputerError ( " focus_changed " , " The element did not take keyboard focus; no key was sent. Observe again. " ) }
763771 }
764772 // Keys go to the app's key window: the observed window, or a dialog, panel or popover above it,
765773 // but never another document window the snapshot did not observe.
@@ -786,7 +794,8 @@ final class Driver {
786794 let position = try point ( p, app: app, snapshot: snapshot) ; let dx = p. deltaX ?? 0 ; let dy = p. deltaY ?? 0
787795 guard dx. isFinite, dy. isFinite, abs ( dx) <= 10000 , abs ( dy) <= 10000 else { throw ComputerError ( " invalid_arguments " , " Scroll deltas exceed bounds. " ) }
788796 guard let event = CGEvent ( scrollWheelEvent2Source: CGEventSource ( stateID: . privateState) , units: . pixel, wheelCount: 2 , wheel1: Int32 ( - dy) , wheel2: Int32 ( - dx) , wheel3: 0 ) else { throw ComputerError ( " input_failed " , " Could not create scroll event. " ) }
789- let windowID = try required ( p. windowId ?? snapshot. windowFrames. first ( where: { $0. value. contains ( position) } ) ? . key, " windowId " )
797+ // Coordinates are window-local to the requested (or selected) window; an element target uses its own.
798+ let windowID = try required ( p. windowId ?? ( p. elementId == nil ? snapshot. selectedWindowId : snapshot. windowFrames. first ( where: { $0. value. contains ( position) } ) ? . key) , " windowId " )
790799 let frame = try required ( snapshot. windowFrames [ windowID] , " window frame " )
791800 try requireForegroundTarget ( pid: pid, windowID: windowID, window: try required ( snapshot. windowElements [ windowID] , " observed window " ) , point: position, frame: frame)
792801 event. location = position; dispatch. willDispatch ( ) ; event. post ( tap: . cghidEventTap)
0 commit comments