AXElements 0.6.0beta1
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +20 -0
- data/LICENSE.txt +25 -0
- data/README.markdown +150 -0
- data/Rakefile +109 -0
- data/docs/AccessibilityTips.markdown +119 -0
- data/docs/Acting.markdown +340 -0
- data/docs/Debugging.markdown +326 -0
- data/docs/Inspecting.markdown +255 -0
- data/docs/KeyboardEvents.markdown +57 -0
- data/docs/NewBehaviour.markdown +151 -0
- data/docs/Notifications.markdown +271 -0
- data/docs/Searching.markdown +250 -0
- data/docs/TestingExtensions.markdown +52 -0
- data/docs/images/AX.png +0 -0
- data/docs/images/all_the_buttons.jpg +0 -0
- data/docs/images/ui_hierarchy.dot +34 -0
- data/docs/images/ui_hierarchy.png +0 -0
- data/ext/key_coder/extconf.rb +6 -0
- data/ext/key_coder/key_coder.m +77 -0
- data/lib/ax_elements/accessibility/enumerators.rb +104 -0
- data/lib/ax_elements/accessibility/language.rb +347 -0
- data/lib/ax_elements/accessibility/qualifier.rb +73 -0
- data/lib/ax_elements/accessibility.rb +164 -0
- data/lib/ax_elements/core.rb +541 -0
- data/lib/ax_elements/element.rb +593 -0
- data/lib/ax_elements/elements/application.rb +88 -0
- data/lib/ax_elements/elements/button.rb +18 -0
- data/lib/ax_elements/elements/radio_button.rb +18 -0
- data/lib/ax_elements/elements/row.rb +30 -0
- data/lib/ax_elements/elements/static_text.rb +17 -0
- data/lib/ax_elements/elements/systemwide.rb +46 -0
- data/lib/ax_elements/inspector.rb +116 -0
- data/lib/ax_elements/macruby_extensions.rb +255 -0
- data/lib/ax_elements/notification.rb +37 -0
- data/lib/ax_elements/version.rb +9 -0
- data/lib/ax_elements.rb +30 -0
- data/lib/minitest/ax_elements.rb +19 -0
- data/lib/mouse.rb +185 -0
- data/lib/rspec/expectations/ax_elements.rb +15 -0
- data/test/elements/test_application.rb +72 -0
- data/test/elements/test_row.rb +27 -0
- data/test/elements/test_systemwide.rb +38 -0
- data/test/helper.rb +119 -0
- data/test/test_accessibility.rb +127 -0
- data/test/test_blankness.rb +26 -0
- data/test/test_core.rb +448 -0
- data/test/test_element.rb +939 -0
- data/test/test_enumerators.rb +81 -0
- data/test/test_inspector.rb +121 -0
- data/test/test_language.rb +157 -0
- data/test/test_macruby_extensions.rb +303 -0
- data/test/test_mouse.rb +5 -0
- data/test/test_search_semantics.rb +143 -0
- metadata +219 -0
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'mouse'
|
2
|
+
|
3
|
+
##
|
4
|
+
# The module that contains helpers for working with the accessibility APIs.
|
5
|
+
module Accessibility; end
|
6
|
+
|
7
|
+
class << Accessibility
|
8
|
+
|
9
|
+
# @group Debug helpers
|
10
|
+
|
11
|
+
##
|
12
|
+
# Get a list of elements, starting with an element you give, and riding
|
13
|
+
# the hierarchy up to the top level object (i.e. the {AX::Application}).
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
#
|
17
|
+
# element = AX::DOCK.list.application_dock_item
|
18
|
+
# Accessibility.path(element) # => [AX::ApplicationDockItem, AX::List, AX::Application]
|
19
|
+
#
|
20
|
+
# @param [AX::Element]
|
21
|
+
# @return [Array<AX::Element>] the path in ascending order
|
22
|
+
def path *elements
|
23
|
+
element = elements.last
|
24
|
+
return path(elements << element.parent) if element.respond_to? :parent
|
25
|
+
return elements
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Make a `dot` format graph of the tree, meant for graphing with
|
30
|
+
# GraphViz.
|
31
|
+
#
|
32
|
+
# @return [String]
|
33
|
+
def graph root
|
34
|
+
raise NotImplementedError, 'Please implement me, :('
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Dump a tree to the console, indenting for each level down the
|
39
|
+
# tree that we go, and inspecting each element.
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
#
|
43
|
+
# puts Accessibility.dump(app)
|
44
|
+
#
|
45
|
+
# @return [String]
|
46
|
+
def dump element
|
47
|
+
output = element.inspect + "\n"
|
48
|
+
enum = Accessibility::DFEnumerator.new(element)
|
49
|
+
enum.each_with_height do |element, depth|
|
50
|
+
output << "\t"*depth + element.inspect + "\n"
|
51
|
+
end
|
52
|
+
output
|
53
|
+
end
|
54
|
+
|
55
|
+
# @group Finding an object at a point
|
56
|
+
|
57
|
+
##
|
58
|
+
# Get the current mouse position and return the top most element at
|
59
|
+
# that point.
|
60
|
+
#
|
61
|
+
# @return [AX::Element]
|
62
|
+
def element_under_mouse
|
63
|
+
element_at_point Mouse.current_position
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# Get the top most object at an arbitrary point on the screen.
|
68
|
+
#
|
69
|
+
# @overload element_at_point(x,y)
|
70
|
+
# @param [Float] x
|
71
|
+
# @param [Float] y
|
72
|
+
#
|
73
|
+
# @overload element_at_point([x,y])
|
74
|
+
# @param [Array(Float,Float)] point
|
75
|
+
#
|
76
|
+
# @overload element_at_point(CGPoint.new(x,y))
|
77
|
+
# @param [CGPoint] point
|
78
|
+
#
|
79
|
+
# @return [AX::Element]
|
80
|
+
def element_at_point *point
|
81
|
+
arg = point.size == 1 ? point.first : point
|
82
|
+
AX::Element.process AX.element_at_point(*arg.to_a.flatten)
|
83
|
+
end
|
84
|
+
alias_method :element_at_position, :element_at_point
|
85
|
+
|
86
|
+
# @group Finding an application object
|
87
|
+
|
88
|
+
##
|
89
|
+
# @todo Find a way for this method to work without sleeping;
|
90
|
+
# consider looping begin/rescue/end until AX starts up
|
91
|
+
# @todo This needs to handle bad bundle identifier's gracefully
|
92
|
+
#
|
93
|
+
# This is the standard way of creating an application object. It will
|
94
|
+
# launch the app if it is not already running and then create the
|
95
|
+
# accessibility object.
|
96
|
+
#
|
97
|
+
# However, this method is a HUGE hack in cases where the app is not
|
98
|
+
# already running; I've tried to register for notifications, launch
|
99
|
+
# synchronously, etc., but there is always a problem with accessibility
|
100
|
+
# not being ready. Hopefully this problem will go away on Lion...
|
101
|
+
#
|
102
|
+
# If this method fails to find an app with the appropriate bundle
|
103
|
+
# identifier then it will return nil, eventually.
|
104
|
+
#
|
105
|
+
# @param [String] bundle a bundle identifier
|
106
|
+
# @param [Float] sleep_time how long to wait between polling
|
107
|
+
# @return [AX::Application,nil]
|
108
|
+
def application_with_bundle_identifier bundle, sleep_time = 2
|
109
|
+
sleep_count = 0
|
110
|
+
while (apps = NSRunningApplication.runningApplicationsWithBundleIdentifier bundle).empty?
|
111
|
+
launch_application bundle
|
112
|
+
return if sleep_count > 10
|
113
|
+
sleep sleep_time
|
114
|
+
sleep_count += 1
|
115
|
+
end
|
116
|
+
application_with_pid apps.first.processIdentifier
|
117
|
+
end
|
118
|
+
|
119
|
+
##
|
120
|
+
# @todo We don't launch apps if they are not running, but we could if
|
121
|
+
# we used `NSWorkspace#launchApplication`, but it will be a headache
|
122
|
+
#
|
123
|
+
# Get the accessibility object for an application given its localized
|
124
|
+
# name. This will not work if the application is not already running.
|
125
|
+
#
|
126
|
+
# @param [String] name name of the application to launch
|
127
|
+
# @return [AX::Application,nil]
|
128
|
+
def application_with_name name
|
129
|
+
NSRunLoop.currentRunLoop.runUntilDate Time.now
|
130
|
+
workspace = NSWorkspace.sharedWorkspace
|
131
|
+
app = workspace.runningApplications.find { |app| app.localizedName == name }
|
132
|
+
application_with_pid(app.processIdentifier) if app
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Get the accessibility object for an application given its PID.
|
137
|
+
#
|
138
|
+
# @return [AX::Application]
|
139
|
+
def application_with_pid pid
|
140
|
+
AX::Element.process AX.application_for_pid pid
|
141
|
+
end
|
142
|
+
|
143
|
+
# @endgroup
|
144
|
+
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
##
|
149
|
+
# This method uses asynchronous method calls to launch applications.
|
150
|
+
#
|
151
|
+
# @param [String] bundle the bundle identifier for the app
|
152
|
+
# @return [Boolean]
|
153
|
+
def launch_application bundle
|
154
|
+
NSWorkspace.sharedWorkspace.launchAppWithBundleIdentifier bundle,
|
155
|
+
options: NSWorkspaceLaunchAsync,
|
156
|
+
additionalEventParamDescriptor: nil,
|
157
|
+
launchIdentifier: nil
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
require 'ax_elements/accessibility/enumerators'
|
164
|
+
require 'ax_elements/accessibility/qualifier'
|
@@ -0,0 +1,541 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Accessibility
|
4
|
+
class << self
|
5
|
+
# @return [Logger]
|
6
|
+
attr_accessor :log
|
7
|
+
end
|
8
|
+
|
9
|
+
@log = Logger.new $stderr
|
10
|
+
@log.level = Logger::ERROR # @todo lame
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# Namespace for all the accessibility objects, as well as core
|
15
|
+
# abstraction layer that that interact with OS X Accessibility
|
16
|
+
# APIs.
|
17
|
+
module AX
|
18
|
+
@ignore_notifs = true
|
19
|
+
@notifs = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# @todo The current strategy dealing with errors is just to log them,
|
24
|
+
# but that may not always be the correct thing to do. The core
|
25
|
+
# has to be refactored around this issue to become more robust.
|
26
|
+
# I've already started doing this for newer APIs but the old ones
|
27
|
+
# are important as well.
|
28
|
+
# @todo I feel a bit weird having to instantiate a new pointer every
|
29
|
+
# time I want to fetch an attribute. Since allocations are costly
|
30
|
+
# it hurts performance a lot when it comes to searches. I wonder if
|
31
|
+
# it would pay off to have a pool of pointers...
|
32
|
+
#
|
33
|
+
# The singleton methods for the AX module represent the core layer of
|
34
|
+
# abstraction for AXElements.
|
35
|
+
#
|
36
|
+
# The methods provide a clean Ruby-ish interface to the low level
|
37
|
+
# CoreFoundation functions that compose the AXAPI. Doing this we can
|
38
|
+
# hide away the need to work with pointers and centralize when errors
|
39
|
+
# are logged from the low level function calls (since CoreFoundation
|
40
|
+
# uses a different pattern for that sort of thing).
|
41
|
+
#
|
42
|
+
# Ideally this API would be stateless, but I'm still working on that...
|
43
|
+
class << AX
|
44
|
+
|
45
|
+
# @group Attributes
|
46
|
+
|
47
|
+
##
|
48
|
+
# List of attributes for the given element.
|
49
|
+
#
|
50
|
+
# @param [AXUIElementRef] element low level accessibility object
|
51
|
+
# @return [Array<String>]
|
52
|
+
def attrs_of_element element
|
53
|
+
ptr = Pointer.new ARRAY
|
54
|
+
code = AXUIElementCopyAttributeNames(element, ptr)
|
55
|
+
log_error element, code unless code.zero?
|
56
|
+
ptr[0]
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Number of elements that would be returned for the given element's
|
61
|
+
# given attribute.
|
62
|
+
#
|
63
|
+
# @param [AXUIElementRef]
|
64
|
+
# @param [String] attr an attribute constant
|
65
|
+
# @return [Fixnum]
|
66
|
+
def attr_count_of_element element, attr
|
67
|
+
ptr = Pointer.new :long_long
|
68
|
+
code = AXUIElementGetAttributeValueCount(element, attr, ptr)
|
69
|
+
log_error element, attr unless code.zero?
|
70
|
+
ptr[0]
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Fetch the given attribute's value from the given element. You will
|
75
|
+
# be given raw data from this method; that is, {Boxed} objects will
|
76
|
+
# still be wrapped in a `AXValueRef`, and elements will be
|
77
|
+
# `AXUIElementRef` objects.
|
78
|
+
#
|
79
|
+
# @param [AXUIElementRef]
|
80
|
+
# @param [String] attr an attribute constant
|
81
|
+
def attr_of_element element, attr
|
82
|
+
ptr = Pointer.new :id
|
83
|
+
code = AXUIElementCopyAttributeValue(element, attr, ptr)
|
84
|
+
log_error element, code unless code.zero?
|
85
|
+
ptr[0]
|
86
|
+
end
|
87
|
+
|
88
|
+
##
|
89
|
+
# @todo Should we handle cases where a subrole has a value of
|
90
|
+
# 'Unknown'? What is the performance impact?
|
91
|
+
#
|
92
|
+
# Fetch subrole and role of an object, pass back an array with the
|
93
|
+
# subrole first if it exists.
|
94
|
+
#
|
95
|
+
# @param [AXUIElementRef]
|
96
|
+
# @param [Array<String>]
|
97
|
+
# @return [Array<String>]
|
98
|
+
def role_for element, attrs
|
99
|
+
ptr = Pointer.new :id
|
100
|
+
AXUIElementCopyAttributeValue(element, ROLE, ptr)
|
101
|
+
ret = [ptr[0]]
|
102
|
+
if attrs.include? SUBROLE
|
103
|
+
AXUIElementCopyAttributeValue(element, SUBROLE, ptr)
|
104
|
+
# Be careful, some things claim to have a subrole but return nil
|
105
|
+
ret.unshift ptr[0] if ptr[0]
|
106
|
+
end
|
107
|
+
#raise "Found an element that has no role: #{CFShow(element)}"
|
108
|
+
ret
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# Ask whether or not the given attribute of a given element can be
|
113
|
+
# changed using the accessibility APIs.
|
114
|
+
#
|
115
|
+
# @param [AXUIElementRef]
|
116
|
+
# @param [String] attr an attribute constant
|
117
|
+
def attr_of_element_writable? element, attr
|
118
|
+
ptr = Pointer.new :bool
|
119
|
+
code = AXUIElementIsAttributeSettable(element, attr, ptr)
|
120
|
+
log_error element, code unless code.zero?
|
121
|
+
ptr[0]
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# @note This method does not check writability of the attribute
|
126
|
+
# you are setting.
|
127
|
+
#
|
128
|
+
# Set the given value to the given attribute of the given element.
|
129
|
+
#
|
130
|
+
# @param [AXUIElementRef] element
|
131
|
+
# @param [String] attr an attribute constant
|
132
|
+
# @param [Object] value the new value to set on the attribute
|
133
|
+
# @return [Object] returns the value that was set
|
134
|
+
def set_attr_of_element element, attr, value
|
135
|
+
code = AXUIElementSetAttributeValue(element, attr, value)
|
136
|
+
log_error element, code unless code.zero?
|
137
|
+
value
|
138
|
+
end
|
139
|
+
|
140
|
+
# @group Actions
|
141
|
+
|
142
|
+
##
|
143
|
+
# List of actions that the given element can perform.
|
144
|
+
#
|
145
|
+
# @param [AXUIElementRef] element low level accessibility object
|
146
|
+
# @return [Array<String>]
|
147
|
+
def actions_of_element element
|
148
|
+
array_ptr = Pointer.new ARRAY
|
149
|
+
code = AXUIElementCopyActionNames(element, array_ptr)
|
150
|
+
log_error element, code unless code.zero?
|
151
|
+
array_ptr[0]
|
152
|
+
end
|
153
|
+
|
154
|
+
##
|
155
|
+
# Trigger the given action for the given element.
|
156
|
+
#
|
157
|
+
# @param [AXUIElementRef] element
|
158
|
+
# @param [String] action an action constant
|
159
|
+
# @return [Boolean] true if successful
|
160
|
+
def action_of_element element, action
|
161
|
+
code = AXUIElementPerformAction(element, action)
|
162
|
+
code.zero? ? true : (log_error(element, code); false)
|
163
|
+
end
|
164
|
+
|
165
|
+
##
|
166
|
+
# In cases where you need (or want) to simulate keyboard input, such as
|
167
|
+
# triggering hotkeys, you will need to use this method.
|
168
|
+
#
|
169
|
+
# See the documentation page on
|
170
|
+
# {file:docs/KeyboardEvents.markdown Keyboard Events}
|
171
|
+
# to get a detailed explanation on how to encode strings.
|
172
|
+
#
|
173
|
+
# @param [AXUIElementRef] element an application to post the event to, or
|
174
|
+
# the system wide accessibility object
|
175
|
+
# @param [String] string the string you want typed on the screen
|
176
|
+
def keyboard_action element, string
|
177
|
+
post_kb_events element, parse_kb_string(string)
|
178
|
+
nil
|
179
|
+
end
|
180
|
+
|
181
|
+
# @group Parameterized Attributes
|
182
|
+
|
183
|
+
##
|
184
|
+
# List of parameterized attributes for the given element.
|
185
|
+
#
|
186
|
+
# @param [AXUIElementRef] element low level accessibility object
|
187
|
+
# @return [Array<String>]
|
188
|
+
def param_attrs_of_element element
|
189
|
+
array_ptr = Pointer.new ARRAY
|
190
|
+
code = AXUIElementCopyParameterizedAttributeNames(element, array_ptr)
|
191
|
+
log_error element, code unless code.zero?
|
192
|
+
array_ptr[0]
|
193
|
+
end
|
194
|
+
|
195
|
+
##
|
196
|
+
# Fetch the given attribute's value from the given element using the given
|
197
|
+
# parameter. You will be given raw data from this method; that is, {Boxed}
|
198
|
+
# objects will still be wrapped in a `AXValueRef`, etc.
|
199
|
+
#
|
200
|
+
# @param [AXUIElementRef] element
|
201
|
+
# @param [String] attr an attribute constant
|
202
|
+
def param_attr_of_element element, attr, param
|
203
|
+
ptr = Pointer.new :id
|
204
|
+
code = AXUIElementCopyParameterizedAttributeValue(element, attr, param, ptr)
|
205
|
+
log_error element, code unless code.zero?
|
206
|
+
ptr[0]
|
207
|
+
end
|
208
|
+
|
209
|
+
# @group Notifications
|
210
|
+
|
211
|
+
##
|
212
|
+
# @todo This method is too big, needs refactoring. It's own class?
|
213
|
+
#
|
214
|
+
# {file:docs/Notifications.markdown Notifications} are a way to put
|
215
|
+
# non-polling delays into your scripts.
|
216
|
+
#
|
217
|
+
# Use this method to register to be notified of the specified event in
|
218
|
+
# an application. You must also pass a block to this method to validate
|
219
|
+
# the notification.
|
220
|
+
#
|
221
|
+
# @param [AXUIElementRef] ref the element which will send the notification
|
222
|
+
# @param [String] name the name of the notification
|
223
|
+
# @yield Validate the notification; the block should return truthy if
|
224
|
+
# the notification received is the expected one and the script can stop
|
225
|
+
# waiting, otherwise should return falsy.
|
226
|
+
# @yieldparam [AXUIElementRef] element the element that sent the notification
|
227
|
+
# @yieldparam [String] notif the name of the notification
|
228
|
+
# @yieldreturn [Boolean] determines if the script should continue or wait
|
229
|
+
# @return [Array(Observer, AXUIElementRef, String)] the registration triple
|
230
|
+
def register_for_notif ref, name, &block
|
231
|
+
run_loop = CFRunLoopGetCurrent()
|
232
|
+
|
233
|
+
# we are ignoring the context pointer since this is OO
|
234
|
+
callback = Proc.new do |observer, element, notif, _|
|
235
|
+
LOCK.synchronize do
|
236
|
+
Accessibility.log.debug "Received notif (#{notif}) for (#{element})"
|
237
|
+
break if @ignore_notifs
|
238
|
+
break unless block.call(element, notif)
|
239
|
+
|
240
|
+
@ignore_notifs = true
|
241
|
+
source = AXObserverGetRunLoopSource(observer)
|
242
|
+
CFRunLoopRemoveSource(run_loop, source, KCFRunLoopDefaultMode)
|
243
|
+
unregister_notif_callback observer, element, notif
|
244
|
+
CFRunLoopStop(run_loop)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
dude = make_observer_for ref, callback
|
249
|
+
source = AXObserverGetRunLoopSource(dude)
|
250
|
+
register_notif_callback dude, ref, name
|
251
|
+
CFRunLoopAddSource(run_loop, source, KCFRunLoopDefaultMode)
|
252
|
+
@ignore_notifs = false
|
253
|
+
|
254
|
+
# must keep [element, observer, notif] in order to do unregistration
|
255
|
+
@notifs[dude] = [ref, name]
|
256
|
+
[dude, ref, name]
|
257
|
+
end
|
258
|
+
|
259
|
+
##
|
260
|
+
# @todo Is it safe to end the run loop when _any_ source is handled or
|
261
|
+
# should we continue to kill the run loop when the callback is
|
262
|
+
# received?
|
263
|
+
#
|
264
|
+
# Pause execution of the program until a notification is received or a
|
265
|
+
# timeout occurs.
|
266
|
+
#
|
267
|
+
# @param [Float] timeout
|
268
|
+
# @return [Boolean] true if the notification was received, otherwise false
|
269
|
+
def wait_for_notif timeout
|
270
|
+
# We use RunInMode because it has timeout functionality, return values are
|
271
|
+
case CFRunLoopRunInMode(KCFRunLoopDefaultMode, timeout, false)
|
272
|
+
when KCFRunLoopRunStopped # Stopped with CFRunLoopStop.
|
273
|
+
true
|
274
|
+
when KCFRunLoopRunTimedOut # Time interval seconds passed.
|
275
|
+
false
|
276
|
+
when KCFRunLoopFinished # Mode has no sources or timers.
|
277
|
+
raise 'Something went wrong with setting up the run loop'
|
278
|
+
when KCFRunLoopRunHandledSource
|
279
|
+
# Only applies when returnAfterSourceHandled is true.
|
280
|
+
raise 'This should never happen'
|
281
|
+
else
|
282
|
+
raise 'You just found a an OS X bug (or a MacRuby bug)...'
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
##
|
287
|
+
# @todo Flush any waiting notifs?
|
288
|
+
#
|
289
|
+
# Cancel _all_ notification registrations. Simple and clean, but a
|
290
|
+
# blunt tool at best. I didn't have time to figure out a better
|
291
|
+
# system :(
|
292
|
+
#
|
293
|
+
# @return [nil]
|
294
|
+
def unregister_notifs
|
295
|
+
LOCK.synchronize do
|
296
|
+
@ignore_notifs = true
|
297
|
+
@notifs.each_pair do |observer, pair|
|
298
|
+
unregister_notif_callback observer, *pair
|
299
|
+
end
|
300
|
+
@notifs = {}
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
# @group Element Entry Points
|
305
|
+
|
306
|
+
##
|
307
|
+
# This will give you the UI element located at the position given. If
|
308
|
+
# more than one element is at the position then the z-order of the
|
309
|
+
# elements will be used to determine which is "on top".
|
310
|
+
#
|
311
|
+
# The coordinates should be specified using the flipped coordinate
|
312
|
+
# system (origin is in the top-left, increasing downward as if reading
|
313
|
+
# a book in English).
|
314
|
+
#
|
315
|
+
# @param [Float]
|
316
|
+
# @param [Float]
|
317
|
+
# @return [AXUIElementRef]
|
318
|
+
def element_at_point x, y
|
319
|
+
ptr = Pointer.new ELEMENT
|
320
|
+
system = AXUIElementCreateSystemWide()
|
321
|
+
code = AXUIElementCopyElementAtPosition(system, x, y, ptr)
|
322
|
+
log_error system, code unless code.zero?
|
323
|
+
ptr[0]
|
324
|
+
end
|
325
|
+
|
326
|
+
##
|
327
|
+
# You can call this method to create the application object given
|
328
|
+
# the process identifier of the app.
|
329
|
+
#
|
330
|
+
# @param [Fixnum] pid process identifier for the application you want
|
331
|
+
# @return [AXUIElementRef]
|
332
|
+
def application_for_pid pid
|
333
|
+
raise ArgumentError, 'pid must be greater than 0' unless pid > 0
|
334
|
+
AXUIElementCreateApplication(pid)
|
335
|
+
end
|
336
|
+
|
337
|
+
# @group Misc.
|
338
|
+
|
339
|
+
##
|
340
|
+
# Get the PID of the application that the given element belongs to.
|
341
|
+
#
|
342
|
+
# @param [AXUIElementRef] element
|
343
|
+
# @return [Fixnum]
|
344
|
+
def pid_of_element element
|
345
|
+
ptr = Pointer.new :int
|
346
|
+
code = AXUIElementGetPid(element, ptr)
|
347
|
+
log_error element, code unless code.zero?
|
348
|
+
ptr[0]
|
349
|
+
end
|
350
|
+
|
351
|
+
# @endgroup
|
352
|
+
|
353
|
+
|
354
|
+
private
|
355
|
+
|
356
|
+
##
|
357
|
+
# @private
|
358
|
+
#
|
359
|
+
# Pointer type encoding for `CFArrayRef` objects.
|
360
|
+
#
|
361
|
+
# @return [String]
|
362
|
+
ARRAY = '^{__CFArray}'.freeze
|
363
|
+
|
364
|
+
##
|
365
|
+
# @private
|
366
|
+
#
|
367
|
+
# Pointer type encoding for `AXUIElementRef` objects.
|
368
|
+
#
|
369
|
+
# @return [String]
|
370
|
+
ELEMENT = '^{__AXUIElement}'.freeze
|
371
|
+
|
372
|
+
##
|
373
|
+
# @private
|
374
|
+
#
|
375
|
+
# Pointer type encoding for `AXObserverRef` objects.
|
376
|
+
#
|
377
|
+
# @return [String]
|
378
|
+
OBSERVER = '^{__AXObserver}'.freeze
|
379
|
+
|
380
|
+
##
|
381
|
+
# @private
|
382
|
+
#
|
383
|
+
# Local copy of a Cocoa constant; this is a performance hack.
|
384
|
+
#
|
385
|
+
# @return [String]
|
386
|
+
ROLE = KAXRoleAttribute
|
387
|
+
|
388
|
+
##
|
389
|
+
# @private
|
390
|
+
#
|
391
|
+
# Local copy of a Cocoa constant; this is a performance hack.
|
392
|
+
#
|
393
|
+
# @return [String]
|
394
|
+
SUBROLE = KAXSubroleAttribute
|
395
|
+
|
396
|
+
# @group Notifications
|
397
|
+
|
398
|
+
##
|
399
|
+
# @todo Would a Dispatch::Semaphore be better?
|
400
|
+
#
|
401
|
+
# Semaphore used to synchronize async notification stuff.
|
402
|
+
#
|
403
|
+
# @return [Mutex]
|
404
|
+
LOCK = Mutex.new
|
405
|
+
|
406
|
+
# @endgroup
|
407
|
+
|
408
|
+
##
|
409
|
+
# Map of characters to keycodes. The map is generated at boot time in
|
410
|
+
# order to support multiple keyboard layouts.
|
411
|
+
#
|
412
|
+
# @return [Hash]
|
413
|
+
KEYCODE_MAP = {}
|
414
|
+
require 'key_coder'
|
415
|
+
|
416
|
+
##
|
417
|
+
# Parse a string into a list of keyboard events to be executed in
|
418
|
+
# the given order.
|
419
|
+
#
|
420
|
+
# @param [String]
|
421
|
+
# @return [Array<Array(Number,Boolean)>]
|
422
|
+
def parse_kb_string string
|
423
|
+
sequence = []
|
424
|
+
string.each_char do |char|
|
425
|
+
if char.match(/[A-Z]/)
|
426
|
+
code = AX::KEYCODE_MAP[char.downcase]
|
427
|
+
event = [[56,true], [code,true], [code,false], [56,false]]
|
428
|
+
else
|
429
|
+
code = AX::KEYCODE_MAP[char]
|
430
|
+
event = [[code,true],[code,false]]
|
431
|
+
end
|
432
|
+
sequence.concat event
|
433
|
+
end
|
434
|
+
sequence
|
435
|
+
end
|
436
|
+
|
437
|
+
##
|
438
|
+
# Post the list of given keyboard events to the given element.
|
439
|
+
#
|
440
|
+
# @param [AXUIElementRef] element must be an application or the
|
441
|
+
# system-wide object
|
442
|
+
# @param [Array<Array(Number,Boolean)>]
|
443
|
+
def post_kb_events element, events
|
444
|
+
events.each do |event|
|
445
|
+
code = AXUIElementPostKeyboardEvent(element, 0, *event)
|
446
|
+
log_error element, code unless code.zero?
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
##
|
451
|
+
# @private
|
452
|
+
#
|
453
|
+
# A mapping of the `AXError` constants to human readable strings, though
|
454
|
+
# this has to be actively maintained in case of changes to Apple's
|
455
|
+
# documentation in the future.
|
456
|
+
#
|
457
|
+
# @return [Hash{Fixnum=>String}]
|
458
|
+
AXError = {
|
459
|
+
KAXErrorFailure => 'Generic Failure',
|
460
|
+
KAXErrorIllegalArgument => 'Illegal Argument',
|
461
|
+
KAXErrorInvalidUIElement => 'Invalid UI Element',
|
462
|
+
KAXErrorInvalidUIElementObserver => 'Invalid UI Element Observer',
|
463
|
+
KAXErrorCannotComplete => 'Cannot Complete',
|
464
|
+
KAXErrorAttributeUnsupported => 'Attribute Unsupported',
|
465
|
+
KAXErrorActionUnsupported => 'Action Unsupported',
|
466
|
+
KAXErrorNotificationUnsupported => 'Notification Unsupported',
|
467
|
+
KAXErrorNotImplemented => 'Not Implemented',
|
468
|
+
KAXErrorNotificationAlreadyRegistered => 'Notification Already Registered',
|
469
|
+
KAXErrorNotificationNotRegistered => 'Notification Not Registered',
|
470
|
+
KAXErrorAPIDisabled => 'API Disabled',
|
471
|
+
KAXErrorNoValue => 'No Value',
|
472
|
+
KAXErrorParameterizedAttributeUnsupported => 'Parameterized Attribute Unsupported',
|
473
|
+
KAXErrorNotEnoughPrecision => 'Not Enough Precision'
|
474
|
+
}
|
475
|
+
|
476
|
+
##
|
477
|
+
# Uses the call stack and error code to log a message that might be
|
478
|
+
# helpful in debugging.
|
479
|
+
#
|
480
|
+
# @param [AXUIElementRef]
|
481
|
+
# @param [Fixnum] code AXError value
|
482
|
+
def log_error element, code
|
483
|
+
message = AXError[code] || 'UNKNOWN ERROR CODE'
|
484
|
+
logger = Accessibility.log
|
485
|
+
logger.warn "[#{message} (#{code})] while trying #{caller[0]}"
|
486
|
+
logger.info "Available attributes were:\n#{attrs_of_element(element)}"
|
487
|
+
logger.info "Available actions were:\n#{actions_of_element(element)}"
|
488
|
+
# @todo logger.info available parameterized attributes
|
489
|
+
logger.debug "Backtrace: #{caller.description}"
|
490
|
+
# @todo logger.debug pp hierarchy element or pp element
|
491
|
+
end
|
492
|
+
|
493
|
+
##
|
494
|
+
# Create and return a notification observer for the given object's
|
495
|
+
# application.
|
496
|
+
#
|
497
|
+
# @param [AXUIElementRef] element
|
498
|
+
# @param [Method,Proc] callback
|
499
|
+
# @return [AXObserverRef]
|
500
|
+
def make_observer_for element, callback
|
501
|
+
ptr = Pointer.new OBSERVER
|
502
|
+
code = AXObserverCreate(pid_of_element(element), callback, ptr)
|
503
|
+
log_error element, code unless code.zero?
|
504
|
+
ptr[0]
|
505
|
+
end
|
506
|
+
|
507
|
+
##
|
508
|
+
# @todo Consider exposing the refcon argument. Probably not until
|
509
|
+
# someone actually wants to pass a context around.
|
510
|
+
#
|
511
|
+
# Register an observer for a specific event.
|
512
|
+
#
|
513
|
+
# @param [AXObserverRef]
|
514
|
+
# @param [AX::Element]
|
515
|
+
# @param [String]
|
516
|
+
def register_notif_callback observer, element, notif
|
517
|
+
code = AXObserverAddNotification(observer, element, notif, nil)
|
518
|
+
log_error element, code unless code.zero?
|
519
|
+
end
|
520
|
+
|
521
|
+
##
|
522
|
+
# @todo No need to capture error code when handling all error cases
|
523
|
+
# properly. So I should get around to that soon.
|
524
|
+
#
|
525
|
+
# Unregister a notification that has been previously setup.
|
526
|
+
#
|
527
|
+
# @param [AXObserverRef]
|
528
|
+
# @param [AX::Element]
|
529
|
+
# @param [String]
|
530
|
+
def unregister_notif_callback observer, ref, notif
|
531
|
+
case code = AXObserverRemoveNotification(observer, ref, notif)
|
532
|
+
when KAXErrorNotificationNotRegistered
|
533
|
+
Accessibility.log.warn "Notif no longer registered: (#{ref}:#{notif})"
|
534
|
+
when KAXErrorIllegalArgument
|
535
|
+
raise ArgumentError, "Notif not unregistered (#{ref}:#{notif})"
|
536
|
+
else
|
537
|
+
log_error element, code unless code.zero?
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
end
|