AXElements 0.6.0beta2 → 0.7.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +1 -2
- data/README.markdown +152 -88
- data/Rakefile +8 -103
- data/docs/Debugging.markdown +9 -2
- data/docs/KeyboardEvents.markdown +114 -49
- data/docs/Setting.markdown +1 -0
- data/docs/images/next_version.png +0 -0
- data/ext/accessibility/key_coder/extconf.rb +22 -0
- data/ext/accessibility/key_coder/key_coder.c +113 -0
- data/lib/AXElements.rb +2 -0
- data/lib/accessibility/core.rb +897 -0
- data/lib/accessibility/debug.rb +168 -0
- data/lib/accessibility/dsl.rb +697 -0
- data/lib/accessibility/enumerators.rb +104 -0
- data/lib/accessibility/errors.rb +32 -0
- data/lib/accessibility/factory.rb +153 -0
- data/lib/accessibility/graph.rb +150 -0
- data/lib/{ax_elements/inspector.rb → accessibility/pp_inspector.rb} +39 -28
- data/lib/accessibility/qualifier.rb +158 -0
- data/lib/accessibility/string.rb +494 -0
- data/lib/accessibility/translator.rb +178 -0
- data/lib/accessibility/version.rb +7 -0
- data/lib/accessibility.rb +79 -0
- data/lib/ax/application.rb +234 -0
- data/lib/{ax_elements/elements → ax}/button.rb +2 -0
- data/lib/ax/element.rb +518 -0
- data/lib/{ax_elements/elements → ax}/radio_button.rb +2 -0
- data/lib/ax/row.rb +37 -0
- data/lib/{ax_elements/elements → ax}/static_text.rb +2 -0
- data/lib/ax/systemwide.rb +86 -0
- data/lib/ax_elements/awesome_print.rb +25 -0
- data/lib/ax_elements/exception_workaround.rb +8 -0
- data/lib/ax_elements/nsarray_compat.rb +64 -0
- data/lib/ax_elements/vendor/inflection_data.rb +65 -0
- data/lib/ax_elements/vendor/inflections.rb +172 -0
- data/lib/ax_elements/vendor/inflector.rb +306 -0
- data/lib/ax_elements.rb +14 -25
- data/lib/minitest/ax_elements.rb +112 -12
- data/lib/mouse.rb +72 -46
- data/lib/rspec/expectations/ax_elements.rb +133 -6
- data/rakelib/doc.rake +13 -0
- data/rakelib/ext.rake +61 -0
- data/rakelib/gem.rake +28 -0
- data/rakelib/test.rake +53 -0
- data/test/helper.rb +11 -97
- data/test/integration/accessibility/test_core.rb +18 -0
- data/test/integration/accessibility/test_debug.rb +44 -0
- data/test/integration/accessibility/test_dsl.rb +225 -0
- data/test/integration/accessibility/test_enumerators.rb +122 -0
- data/test/integration/accessibility/test_errors.rb +38 -0
- data/test/integration/accessibility/test_notifications.rb +22 -0
- data/test/integration/accessibility/test_qualifier.rb +148 -0
- data/test/integration/ax/test_application.rb +56 -0
- data/test/integration/ax/test_element.rb +46 -0
- data/test/integration/ax/test_row.rb +23 -0
- data/test/integration/ax_elements/test_nsarray_compat.rb +43 -0
- data/test/integration/minitest/test_ax_elements.rb +98 -0
- data/test/integration/rspec/expectations/test_ax_elements.rb +58 -0
- data/test/integration/test_mouse.rb +35 -0
- data/test/sanity/accessibility/test_core.rb +553 -0
- data/test/sanity/accessibility/test_debug.rb +63 -0
- data/test/sanity/accessibility/test_dsl.rb +75 -0
- data/test/sanity/accessibility/test_errors.rb +10 -0
- data/test/sanity/accessibility/test_factory.rb +88 -0
- data/test/sanity/accessibility/test_pp_inspector.rb +110 -0
- data/test/sanity/accessibility/test_qualifier.rb +13 -0
- data/test/sanity/accessibility/test_string.rb +238 -0
- data/test/sanity/accessibility/test_translator.rb +145 -0
- data/test/sanity/ax/test_application.rb +90 -0
- data/test/sanity/ax/test_element.rb +80 -0
- data/test/sanity/ax/test_systemwide.rb +66 -0
- data/test/sanity/ax_elements/test_nsarray_compat.rb +16 -0
- data/test/sanity/ax_elements/test_nsobject_inspect.rb +11 -0
- data/test/sanity/minitest/test_ax_elements.rb +15 -0
- data/test/sanity/rspec/expectations/test_ax_elements.rb +12 -0
- data/test/sanity/test_ax_elements.rb +10 -0
- data/test/sanity/test_mouse.rb +19 -0
- metadata +111 -93
- data/LICENSE.txt +0 -25
- data/ext/key_coder/extconf.rb +0 -6
- data/ext/key_coder/key_coder.m +0 -77
- data/lib/ax_elements/accessibility/enumerators.rb +0 -104
- data/lib/ax_elements/accessibility/graph.rb +0 -118
- data/lib/ax_elements/accessibility/language.rb +0 -347
- data/lib/ax_elements/accessibility/qualifier.rb +0 -73
- data/lib/ax_elements/accessibility.rb +0 -166
- data/lib/ax_elements/core.rb +0 -541
- data/lib/ax_elements/element.rb +0 -593
- data/lib/ax_elements/elements/application.rb +0 -88
- data/lib/ax_elements/elements/row.rb +0 -30
- data/lib/ax_elements/elements/systemwide.rb +0 -46
- data/lib/ax_elements/macruby_extensions.rb +0 -255
- data/lib/ax_elements/notification.rb +0 -37
- data/lib/ax_elements/version.rb +0 -9
- data/test/elements/test_application.rb +0 -72
- data/test/elements/test_row.rb +0 -27
- data/test/elements/test_systemwide.rb +0 -38
- data/test/test_accessibility.rb +0 -127
- data/test/test_blankness.rb +0 -26
- data/test/test_core.rb +0 -448
- data/test/test_element.rb +0 -939
- data/test/test_enumerators.rb +0 -81
- data/test/test_inspector.rb +0 -130
- data/test/test_language.rb +0 -157
- data/test/test_macruby_extensions.rb +0 -303
- data/test/test_mouse.rb +0 -5
- data/test/test_search_semantics.rb +0 -143
@@ -0,0 +1,897 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
framework 'Cocoa'
|
4
|
+
|
5
|
+
# check that the Accessibility APIs are enabled and are available to MacRuby
|
6
|
+
begin
|
7
|
+
unless AXAPIEnabled()
|
8
|
+
raise RuntimeError, <<-EOS
|
9
|
+
------------------------------------------------------------------------
|
10
|
+
Universal Access is disabled on this machine.
|
11
|
+
|
12
|
+
Please enable it in the System Preferences.
|
13
|
+
------------------------------------------------------------------------
|
14
|
+
EOS
|
15
|
+
end
|
16
|
+
rescue NoMethodError
|
17
|
+
raise NotImplementedError, <<-EOS
|
18
|
+
------------------------------------------------------------------------
|
19
|
+
You need to install the latest BridgeSupport preview so that AXElements
|
20
|
+
has access to CoreFoundation.
|
21
|
+
------------------------------------------------------------------------
|
22
|
+
EOS
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
require 'accessibility/version'
|
27
|
+
|
28
|
+
##
|
29
|
+
# @todo Slowly back off on raising exceptions in error conditions. Most
|
30
|
+
# often we can just return nil or an empty array and it should all
|
31
|
+
# still work out ok.
|
32
|
+
# @todo I feel a bit weird having to instantiate a new pointer every
|
33
|
+
# time I want to fetch an attribute. Since allocations are costly,
|
34
|
+
# it hurts performance a lot when it comes to searches. I wonder if
|
35
|
+
# it would pay off to have a pool of pointers...
|
36
|
+
#
|
37
|
+
# Core abstraction layer that that interacts with OS X Accessibility
|
38
|
+
# APIs (AXAPI). This is actually just a mixin for `AXUIElementRef` objects
|
39
|
+
# so that they become more object oriented.
|
40
|
+
#
|
41
|
+
# This module is responsible for handling pointers and dealing with error
|
42
|
+
# codes for functions that make use of them. The methods in this module
|
43
|
+
# provide a clean Ruby-ish interface to the low level CoreFoundation
|
44
|
+
# functions that compose AXAPI. In doing this, we can hide away the need
|
45
|
+
# to work with pointers and centralize how AXAPI related errors are handled
|
46
|
+
# (since CoreFoundation uses a different pattern for that sort of thing).
|
47
|
+
#
|
48
|
+
# @example
|
49
|
+
#
|
50
|
+
# element = AXUIElementCreateSystemWide()
|
51
|
+
# element.attributes # => ["AXRole", "AXChildren", ...]
|
52
|
+
# element.size_of "AXChildren" # => 12
|
53
|
+
#
|
54
|
+
module Accessibility::Core
|
55
|
+
|
56
|
+
|
57
|
+
# @group Attributes
|
58
|
+
|
59
|
+
##
|
60
|
+
# @todo Invalid elements do not always raise an error.
|
61
|
+
# This is a bug that should be logged with Apple.
|
62
|
+
#
|
63
|
+
# Get the list of attributes for the element. As a convention, this
|
64
|
+
# method will return an empty array if the backing element is no longer
|
65
|
+
# alive.
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
#
|
69
|
+
# attributes # => ["AXRole", "AXRoleDescription", ...]
|
70
|
+
#
|
71
|
+
# @return [Array<String>]
|
72
|
+
def attributes
|
73
|
+
@attributes ||= (
|
74
|
+
ptr = Pointer.new ARRAY
|
75
|
+
case code = AXUIElementCopyAttributeNames(self, ptr)
|
76
|
+
when 0 then ptr.value
|
77
|
+
when KAXErrorInvalidUIElement then []
|
78
|
+
else handle_error code
|
79
|
+
end
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Fetch the value for an attribute. CoreFoundation wrapped objects
|
85
|
+
# will be unwrapped for you, if you expect to get a {CFRange} you
|
86
|
+
# will be given a {Range} instead.
|
87
|
+
#
|
88
|
+
# As a convention, if the backing element is no longer alive then
|
89
|
+
# you will receive `nil` for any attribute.
|
90
|
+
#
|
91
|
+
# @example
|
92
|
+
# attribute KAXTitleAttribute # => "HotCocoa Demo"
|
93
|
+
# attribute KAXSizeAttribute # => #<CGSize width=10.0 height=88>
|
94
|
+
# attribute KAXParentAttribute # => #<AXUIElementRef>
|
95
|
+
# attribute KAXNoValueAttribute # => nil
|
96
|
+
#
|
97
|
+
# @param [String] name an attribute constant
|
98
|
+
def attribute name
|
99
|
+
ptr = Pointer.new :id
|
100
|
+
case code = AXUIElementCopyAttributeValue(self, name, ptr)
|
101
|
+
when 0 then ptr.value.to_ruby
|
102
|
+
when KAXErrorNoValue then nil
|
103
|
+
when KAXErrorInvalidUIElement
|
104
|
+
name == KAXChildrenAttribute ? [] : nil
|
105
|
+
when KAXErrorFailure
|
106
|
+
name == KAXChildrenAttribute ? [] : handle_error(code, name)
|
107
|
+
else handle_error code, name
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# Shortcut for getting the `KAXRoleAttribute`.
|
113
|
+
#
|
114
|
+
# @example
|
115
|
+
#
|
116
|
+
# role # => KAXWindowRole
|
117
|
+
#
|
118
|
+
# @return [String]
|
119
|
+
def role
|
120
|
+
attribute KAXRoleAttribute
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# @note You might get `nil` back as the subrole as AXWebArea
|
125
|
+
# objects are known to do this. You need to check. :(
|
126
|
+
#
|
127
|
+
# Shortcut for getting the `KAXSubroleAttribute`.
|
128
|
+
#
|
129
|
+
# @example
|
130
|
+
# subrole # => "AXDialog"
|
131
|
+
# subrole # => nil
|
132
|
+
#
|
133
|
+
# @return [String,nil]
|
134
|
+
def subrole
|
135
|
+
attribute KAXSubroleAttribute
|
136
|
+
end
|
137
|
+
|
138
|
+
##
|
139
|
+
# Shortcut for getting the `KAXChildrenAttribute`.
|
140
|
+
#
|
141
|
+
# @example
|
142
|
+
#
|
143
|
+
# children # => [MenuBar, Window, ...]
|
144
|
+
#
|
145
|
+
# @return [Array<AX::Element>]
|
146
|
+
def children
|
147
|
+
attribute KAXChildrenAttribute
|
148
|
+
end
|
149
|
+
|
150
|
+
##
|
151
|
+
# Shortcut for getting the `KAXValueAttribute`.
|
152
|
+
#
|
153
|
+
# @example
|
154
|
+
#
|
155
|
+
# value # => "Mark Rada"
|
156
|
+
# value # => 42
|
157
|
+
#
|
158
|
+
def value
|
159
|
+
attribute KAXValueAttribute
|
160
|
+
end
|
161
|
+
|
162
|
+
##
|
163
|
+
# Get the size of the array for attributes that would return an array.
|
164
|
+
# When performance matters, this is much faster than getting the array
|
165
|
+
# and asking for the size.
|
166
|
+
#
|
167
|
+
# If there is a failure or the backing element is no longer alive, this
|
168
|
+
# method will return `0`.
|
169
|
+
#
|
170
|
+
# @example
|
171
|
+
#
|
172
|
+
# size_of KAXChildrenAttribute # => 19
|
173
|
+
# size_of KAXRowsAttribute # => 100
|
174
|
+
#
|
175
|
+
# @param [String] name an attribute constant
|
176
|
+
# @return [Number]
|
177
|
+
def size_of name
|
178
|
+
ptr = Pointer.new :long_long
|
179
|
+
case code = AXUIElementGetAttributeValueCount(self, name, ptr)
|
180
|
+
when 0 then ptr.value
|
181
|
+
when KAXErrorFailure, KAXErrorAttributeUnsupported,
|
182
|
+
KAXErrorInvalidUIElement then 0
|
183
|
+
else handle_error code, name
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
##
|
188
|
+
# Returns whether or not an attribute is writable.
|
189
|
+
#
|
190
|
+
# @example
|
191
|
+
#
|
192
|
+
# writable? KAXSizeAttribute # => true
|
193
|
+
# writable? KAXTitleAttribute # => false
|
194
|
+
#
|
195
|
+
# @param [String] name an attribute constant
|
196
|
+
def writable? name
|
197
|
+
ptr = Pointer.new :bool
|
198
|
+
case code = AXUIElementIsAttributeSettable(self, name, ptr)
|
199
|
+
when 0 then ptr.value
|
200
|
+
when KAXErrorInvalidUIElement then false
|
201
|
+
else handle_error code, name
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
##
|
206
|
+
# @note This method does not check writability of the attribute
|
207
|
+
# you are setting. If you need to check, use {#writable?}
|
208
|
+
# first.
|
209
|
+
#
|
210
|
+
# Set the given value to the given attribute. You do not need to
|
211
|
+
# worry about wrapping objects first, `Range` objects will also
|
212
|
+
# be automatically converted into `CFRange` objects and then
|
213
|
+
# wrapped.
|
214
|
+
#
|
215
|
+
# Unlike when reading attributes, writing to a dead element will
|
216
|
+
# raise an exception.
|
217
|
+
#
|
218
|
+
# @example
|
219
|
+
# set KAXValueAttribute, "hi" # => "hi"
|
220
|
+
# set KAXSizeAttribute, [250,250] # => [250,250]
|
221
|
+
# set KAXVisibleRangeAttribute, 0..-3 # => 0..-3
|
222
|
+
#
|
223
|
+
# @param [String] name an attribute constant
|
224
|
+
def set name, value
|
225
|
+
code = AXUIElementSetAttributeValue(self, name, value.to_ax)
|
226
|
+
return value if code.zero?
|
227
|
+
handle_error code, name, value
|
228
|
+
end
|
229
|
+
|
230
|
+
|
231
|
+
# @group Parameterized Attributes
|
232
|
+
|
233
|
+
##
|
234
|
+
# Get the list of parameterized attributes for the element. If the
|
235
|
+
# element does not have parameterized attributes, then an empty
|
236
|
+
# list will be returned.
|
237
|
+
#
|
238
|
+
# Most elements do not have parameterized attributes, but the ones
|
239
|
+
# that do, have many.
|
240
|
+
#
|
241
|
+
# @example
|
242
|
+
#
|
243
|
+
# parameterized_attributes # => ["AXStringForRange", ...]
|
244
|
+
# parameterized_attributes # => []
|
245
|
+
#
|
246
|
+
# @return [Array<String>]
|
247
|
+
def parameterized_attributes
|
248
|
+
@parameterized_attributes ||= (
|
249
|
+
ptr = Pointer.new ARRAY
|
250
|
+
case code = AXUIElementCopyParameterizedAttributeNames(self, ptr)
|
251
|
+
when 0 then ptr.value
|
252
|
+
when KAXErrorNoValue, KAXErrorInvalidUIElement then []
|
253
|
+
else handle_error code
|
254
|
+
end
|
255
|
+
)
|
256
|
+
end
|
257
|
+
|
258
|
+
##
|
259
|
+
# Fetch the given pramaeterized attribute value using the given parameter.
|
260
|
+
# Only `AXUIElementRef` objects will be given raw, `Boxed` objects will be
|
261
|
+
# unwrapped for you automatically and `CFRange` objects will be turned into
|
262
|
+
# `Range` objects. Similarly, you do not need to worry about wrapping the
|
263
|
+
# parameter as that will be done for you.
|
264
|
+
#
|
265
|
+
# @example
|
266
|
+
#
|
267
|
+
# attribute KAXStringForRangeParameterizedAttribute, for_param: 1..10
|
268
|
+
# # => "ello, worl"
|
269
|
+
#
|
270
|
+
# @param [String] attr an attribute constant
|
271
|
+
# @param [Object] param
|
272
|
+
def attribute name, for_parameter: param
|
273
|
+
ptr = Pointer.new :id
|
274
|
+
param = param.to_ax
|
275
|
+
case code = AXUIElementCopyParameterizedAttributeValue(self,name,param,ptr)
|
276
|
+
when 0 then ptr.value.to_ruby
|
277
|
+
when KAXErrorNoValue, KAXErrorInvalidUIElement then nil
|
278
|
+
else handle_error code, name, param
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
|
283
|
+
# @group Actions
|
284
|
+
|
285
|
+
##
|
286
|
+
# Get the list of actions that the element can perform. If an element
|
287
|
+
# does not have actions, then an empty list will be returned.
|
288
|
+
# Dead elements will also return an empty array.
|
289
|
+
#
|
290
|
+
# @example
|
291
|
+
#
|
292
|
+
# action_names # => ["AXPress"]
|
293
|
+
#
|
294
|
+
# @return [Array<String>]
|
295
|
+
def actions
|
296
|
+
@actions ||= (
|
297
|
+
ptr = Pointer.new ARRAY
|
298
|
+
case code = AXUIElementCopyActionNames(self, ptr)
|
299
|
+
when 0 then ptr.value
|
300
|
+
when KAXErrorInvalidUIElement then []
|
301
|
+
else handle_error code
|
302
|
+
end
|
303
|
+
)
|
304
|
+
end
|
305
|
+
|
306
|
+
##
|
307
|
+
# Ask an element to perform the given action. This method will always
|
308
|
+
# return true or raise an exception. Actions should never fail.
|
309
|
+
#
|
310
|
+
# Unlike when reading attributes, performing an action on a dead element
|
311
|
+
# will raise an exception.
|
312
|
+
#
|
313
|
+
# @example
|
314
|
+
#
|
315
|
+
# perform KAXPressAction # => true
|
316
|
+
#
|
317
|
+
# @param [String] action an action constant
|
318
|
+
# @return [Boolean]
|
319
|
+
def perform action
|
320
|
+
code = AXUIElementPerformAction(self, action)
|
321
|
+
return true if code.zero?
|
322
|
+
handle_error code, action
|
323
|
+
end
|
324
|
+
|
325
|
+
##
|
326
|
+
# Post the list of given keyboard events to the element. This only
|
327
|
+
# applies if the given element is an application object or the
|
328
|
+
# system wide object.
|
329
|
+
#
|
330
|
+
# Events could be generated from a string using output from
|
331
|
+
# {Accessibility::String#keyboard_events_for}.
|
332
|
+
#
|
333
|
+
# Events are number/boolean tuples, where the number is a keycode
|
334
|
+
# and the boolean is the keypress state (true is keydown, false is
|
335
|
+
# keyup).
|
336
|
+
#
|
337
|
+
# You can learn more about keyboard events from the
|
338
|
+
# {file:docs/KeyboardEvents.markdown Keyboard Events} documentation.
|
339
|
+
#
|
340
|
+
# @example
|
341
|
+
#
|
342
|
+
# include Accessibility::String
|
343
|
+
# events = keyboard_events_for "Hello, world!\n"
|
344
|
+
# post events
|
345
|
+
#
|
346
|
+
# @param [Array<Array(Number,Boolean)>]
|
347
|
+
# @param [AXUIElementRef]
|
348
|
+
def post events
|
349
|
+
events.each do |event|
|
350
|
+
code = AXUIElementPostKeyboardEvent(self, 0, *event)
|
351
|
+
handle_error code unless code.zero?
|
352
|
+
sleep KEY_RATE
|
353
|
+
end
|
354
|
+
sleep 0.1 # in many cases, UI is not done updating right away
|
355
|
+
end
|
356
|
+
|
357
|
+
##
|
358
|
+
# @todo Make this runtime configurable.
|
359
|
+
#
|
360
|
+
# The delay between key presses. The default value is `0.01`, which
|
361
|
+
# should be about 50 characters per second (down and up are separate
|
362
|
+
# events).
|
363
|
+
#
|
364
|
+
# This is just a magic number from trial and error. Both the repeat
|
365
|
+
# interval (NXKeyRepeatInterval) and threshold (NXKeyRepeatThreshold),
|
366
|
+
# but both were way too big.
|
367
|
+
#
|
368
|
+
# @return [Number]
|
369
|
+
KEY_RATE = case ENV['KEY_RATE']
|
370
|
+
when 'VERY_SLOW' then 0.9
|
371
|
+
when 'SLOW' then 0.09
|
372
|
+
when nil then 0.009
|
373
|
+
else ENV['KEY_RATE'].to_f
|
374
|
+
end
|
375
|
+
|
376
|
+
|
377
|
+
# @group Element Hierarchy Entry Points
|
378
|
+
|
379
|
+
##
|
380
|
+
# Find the top most element at a point on the screen that belongs to the
|
381
|
+
# backing application. If the backing element is the system wide object
|
382
|
+
# then the return is the top most element regardless of application.
|
383
|
+
#
|
384
|
+
# The coordinates should be specified using the flipped coordinate
|
385
|
+
# system (origin is in the top-left, increasing downward and to the right
|
386
|
+
# as if reading a book in English).
|
387
|
+
#
|
388
|
+
# If more than one element is at the position then the
|
389
|
+
# z-order of the elements will be used to determine which is
|
390
|
+
# "on top".
|
391
|
+
#
|
392
|
+
# @example
|
393
|
+
#
|
394
|
+
# element_at [453, 200] # table
|
395
|
+
#
|
396
|
+
# @param [#to_point]
|
397
|
+
# @return [AXUIElementRef,nil]
|
398
|
+
def element_at point
|
399
|
+
ptr = Pointer.new ELEMENT
|
400
|
+
case code = AXUIElementCopyElementAtPosition(self, *point.to_point, ptr)
|
401
|
+
when 0 then ptr.value
|
402
|
+
when KAXErrorNoValue then nil
|
403
|
+
else handle_error code, point, nil, nil
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
##
|
408
|
+
# Get the application accessibility object/token for an application
|
409
|
+
# given the process identifier (PID) for that application.
|
410
|
+
#
|
411
|
+
# @example
|
412
|
+
#
|
413
|
+
# app = application_for 54743 # => #<AXUIElementRefx00000000>
|
414
|
+
# CFShow(app)
|
415
|
+
#
|
416
|
+
# @param [Fixnum]
|
417
|
+
# @return [AXUIElementRef]
|
418
|
+
def application_for pid
|
419
|
+
spin_run_loop
|
420
|
+
if NSRunningApplication.runningApplicationWithProcessIdentifier pid
|
421
|
+
AXUIElementCreateApplication(pid)
|
422
|
+
else
|
423
|
+
raise ArgumentError, 'pid must belong to a running application'
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
|
428
|
+
# @group Notifications
|
429
|
+
|
430
|
+
##
|
431
|
+
# @todo Allow a `Method` object to be passed once MacRuby ticket #1463
|
432
|
+
# is fixed.
|
433
|
+
#
|
434
|
+
# Create and return a notification observer for the given object's
|
435
|
+
# application. You should give a block to this method that accepts three
|
436
|
+
# parameters: the observer, the notification sender, and the notification
|
437
|
+
# name.
|
438
|
+
#
|
439
|
+
# Observer's belong to an application, so you can cache a particular
|
440
|
+
# observer and use it for many different notification registrations.
|
441
|
+
#
|
442
|
+
# @example
|
443
|
+
#
|
444
|
+
# observer do |obsrvr, sender, notif|
|
445
|
+
# # do stuff...
|
446
|
+
# end
|
447
|
+
#
|
448
|
+
# @yieldparam [AXObserverRef]
|
449
|
+
# @yieldparam [AXUIElementRef]
|
450
|
+
# @yieldparam [String]
|
451
|
+
# @return [AXObserverRef]
|
452
|
+
def observer
|
453
|
+
raise ArgumentError, 'A callback is required' unless block_given?
|
454
|
+
ptr = Pointer.new OBSERVER
|
455
|
+
callback = proc { |obsrvr, sender, notif, ctx| yield obsrvr, sender, notif }
|
456
|
+
case code = AXObserverCreate(pid, callback, ptr)
|
457
|
+
when 0 then ptr.value
|
458
|
+
else handle_error code, callback
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
##
|
463
|
+
# Get the run loop source for the given observer. You will need to
|
464
|
+
# get the source for an observer added the a run loop source in
|
465
|
+
# your script in order to begin receiving notifications.
|
466
|
+
#
|
467
|
+
# @example
|
468
|
+
#
|
469
|
+
# # get the source
|
470
|
+
# source = run_loop_source_for observer
|
471
|
+
#
|
472
|
+
# # add the source to the current run loop
|
473
|
+
# CFRunLoopAddSource(CFRunLoopGetCurrent(), source, KCFRunLoopDefaultMode)
|
474
|
+
#
|
475
|
+
# # don't forget to remove the source when you are done!
|
476
|
+
#
|
477
|
+
# @param [AXObserverRef]
|
478
|
+
# @return [CFRunLoopSourceRef]
|
479
|
+
def run_loop_source_for observer
|
480
|
+
AXObserverGetRunLoopSource(observer)
|
481
|
+
end
|
482
|
+
|
483
|
+
##
|
484
|
+
# @todo Should passing around a context be supported?
|
485
|
+
#
|
486
|
+
# Register a notification observer for a specific event.
|
487
|
+
#
|
488
|
+
# @example
|
489
|
+
#
|
490
|
+
# register observer, to_receive: KAXWindowCreatedNotification
|
491
|
+
#
|
492
|
+
# @param [AXObserverRef]
|
493
|
+
# @param [String]
|
494
|
+
# @return [Boolean]
|
495
|
+
def register observer, to_receive: notif
|
496
|
+
case code = AXObserverAddNotification(observer, self, notif, nil)
|
497
|
+
when 0 then true
|
498
|
+
else handle_error code, notif, observer, nil, nil
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
##
|
503
|
+
# Unregister a notification that has been previously setup.
|
504
|
+
#
|
505
|
+
# @param [AXObserverRef]
|
506
|
+
# @param [String]
|
507
|
+
# @return [Boolean]
|
508
|
+
def unregister observer, from_receiving: notif
|
509
|
+
case code = AXObserverRemoveNotification(observer, self, notif)
|
510
|
+
when 0 then true
|
511
|
+
else handle_error code, notif, observer, nil, nil
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
|
516
|
+
# @group Misc.
|
517
|
+
|
518
|
+
##
|
519
|
+
# Ask whether or not AXAPI is enabled.
|
520
|
+
#
|
521
|
+
# @example
|
522
|
+
#
|
523
|
+
# enabled? # => true
|
524
|
+
#
|
525
|
+
# # After unchecking "Enable access for assistive devices" in System Prefs
|
526
|
+
# enabled? # => false
|
527
|
+
#
|
528
|
+
def enabled?
|
529
|
+
AXAPIEnabled()
|
530
|
+
end
|
531
|
+
|
532
|
+
##
|
533
|
+
# Get the process identifier (PID) of the application that the element
|
534
|
+
# belongs to.
|
535
|
+
#
|
536
|
+
# @example
|
537
|
+
#
|
538
|
+
# pid # => 12345
|
539
|
+
#
|
540
|
+
# @return [Fixnum]
|
541
|
+
def pid
|
542
|
+
@pid ||= (
|
543
|
+
ptr = Pointer.new :int
|
544
|
+
case code = AXUIElementGetPid(self, ptr)
|
545
|
+
when 0 then ptr.value
|
546
|
+
when KAXErrorInvalidUIElement
|
547
|
+
self == system_wide ? 0 : handle_error(code)
|
548
|
+
else handle_error code
|
549
|
+
end
|
550
|
+
)
|
551
|
+
end
|
552
|
+
|
553
|
+
##
|
554
|
+
# Create a new reference to the system wide object. This is very useful when
|
555
|
+
# working with the system wide object as caching the system wide reference
|
556
|
+
# does not seem to work often.
|
557
|
+
#
|
558
|
+
# @example
|
559
|
+
#
|
560
|
+
# system_wide # => #<AXUIElementRefx00000000>
|
561
|
+
#
|
562
|
+
# @return [AXUIElementRef]
|
563
|
+
def system_wide
|
564
|
+
AXUIElementCreateSystemWide()
|
565
|
+
end
|
566
|
+
|
567
|
+
##
|
568
|
+
# Returns the application reference that the element belongs to.
|
569
|
+
#
|
570
|
+
# @return [AXUIElementRef]
|
571
|
+
def application
|
572
|
+
application_for pid
|
573
|
+
end
|
574
|
+
|
575
|
+
##
|
576
|
+
# Spin the run loop once. For the purpose of receiving notification
|
577
|
+
# callbacks and other Cocoa methods that depend on a run loop.
|
578
|
+
#
|
579
|
+
# @example
|
580
|
+
#
|
581
|
+
# spin_run_loop # not much to it
|
582
|
+
#
|
583
|
+
# @return [self] returns the receiver
|
584
|
+
def spin_run_loop
|
585
|
+
NSRunLoop.currentRunLoop.runUntilDate Time.now
|
586
|
+
end
|
587
|
+
|
588
|
+
|
589
|
+
# @group Debug
|
590
|
+
|
591
|
+
##
|
592
|
+
# Change the timeout value for the element. If you change the timeout
|
593
|
+
# on the system wide object, it affets all timeouts.
|
594
|
+
#
|
595
|
+
# Setting the global timeout to `0` seconds will reset the timeout value
|
596
|
+
# to the system default. Apple does not appear to have publicly documented
|
597
|
+
# what the system default is though, so I can't tell you what that value
|
598
|
+
# is.
|
599
|
+
#
|
600
|
+
# @param [Number]
|
601
|
+
# @return [Number]
|
602
|
+
def set_timeout_to seconds
|
603
|
+
case code = AXUIElementSetMessagingTimeout(self, seconds)
|
604
|
+
when 0 then seconds
|
605
|
+
else handle_error code, seconds
|
606
|
+
end
|
607
|
+
end
|
608
|
+
|
609
|
+
|
610
|
+
private
|
611
|
+
|
612
|
+
# @group Error Handling
|
613
|
+
|
614
|
+
# @param [Number]
|
615
|
+
def handle_error code, *args
|
616
|
+
klass, handler = AXERROR.fetch code, [RuntimeError, :handle_unknown]
|
617
|
+
msg = if handler == :handle_unknown
|
618
|
+
"You should never reach this line [#{code}]:#{inspect}"
|
619
|
+
else
|
620
|
+
self.send handler, *args
|
621
|
+
end
|
622
|
+
raise klass, msg, caller(1)
|
623
|
+
end
|
624
|
+
|
625
|
+
def handle_failure *args
|
626
|
+
"A system failure occurred with #{inspect}, stopping to be safe"
|
627
|
+
end
|
628
|
+
|
629
|
+
def handle_illegal_argument *args
|
630
|
+
case args.size
|
631
|
+
when 0
|
632
|
+
"#{inspect} is not an AXUIElementRef"
|
633
|
+
when 1
|
634
|
+
"Either the element #{inspect} " +
|
635
|
+
"or the attribute/action/callback #{args.first.inspect} " +
|
636
|
+
"is not a legal argument"
|
637
|
+
when 2
|
638
|
+
"You can't get/set #{args.first.inspect} with/to " +
|
639
|
+
"#{args[1].inspect} for #{inspect}"
|
640
|
+
when 3
|
641
|
+
"The point #{args.first.to_point.inspect} is not a valid point, " +
|
642
|
+
"or #{inspect} is not an AXUIElementRef"
|
643
|
+
when 4
|
644
|
+
"Either the observer #{args[1].inspect}, " +
|
645
|
+
"the element #{inspect}, or " +
|
646
|
+
"the notification #{args.first.inspect} " +
|
647
|
+
"is not a legitimate argument"
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
def handle_invalid_element *args
|
652
|
+
"#{inspect} is no longer a valid reference"
|
653
|
+
end
|
654
|
+
|
655
|
+
def handle_invalid_observer *args
|
656
|
+
"#{args[1].inspect} is no longer a valid observer for " +
|
657
|
+
"#{inspect} or was never valid"
|
658
|
+
end
|
659
|
+
|
660
|
+
# @param [AXUIElementRef]
|
661
|
+
def handle_cannot_complete *args
|
662
|
+
spin_run_loop
|
663
|
+
app = NSRunningApplication.runningApplicationWithProcessIdentifier pid
|
664
|
+
if app
|
665
|
+
"An unspecified error occurred using #{inspect} with AXAPI" +
|
666
|
+
", maybe a timeout :("
|
667
|
+
else
|
668
|
+
"Application for pid=#{pid} is no longer running. Maybe it crashed?"
|
669
|
+
end
|
670
|
+
end
|
671
|
+
|
672
|
+
def handle_attr_unsupported *args
|
673
|
+
"#{inspect} does not have a #{args.first.inspect} attribute"
|
674
|
+
end
|
675
|
+
|
676
|
+
def handle_action_unsupported *args
|
677
|
+
"#{inspect} does not have a #{args.first.inspect} action"
|
678
|
+
end
|
679
|
+
|
680
|
+
def handle_notif_unsupported *args
|
681
|
+
"#{inspect} does not support the #{args.first.inspect} notification"
|
682
|
+
end
|
683
|
+
|
684
|
+
def handle_not_implemented *args
|
685
|
+
"The program that owns #{inspect} does not work with AXAPI properly"
|
686
|
+
end
|
687
|
+
|
688
|
+
# @todo Does this really neeed to raise an exception? Seems
|
689
|
+
# like a warning would be sufficient.
|
690
|
+
def handle_notif_registered *args
|
691
|
+
"You have already registered to hear about #{args[0].inspect} " +
|
692
|
+
"from #{inspect}"
|
693
|
+
end
|
694
|
+
|
695
|
+
def handle_notif_not_registered *args
|
696
|
+
"You have not registered to hear about #{args[0].inspect} " +
|
697
|
+
"from #{inspect}"
|
698
|
+
end
|
699
|
+
|
700
|
+
def handle_api_disabled *args
|
701
|
+
'AXAPI has been disabled'
|
702
|
+
end
|
703
|
+
|
704
|
+
def handle_param_attr_unsupported *args
|
705
|
+
"#{inspect} does not have a #{args[0].inspect} parameterized attribute"
|
706
|
+
end
|
707
|
+
|
708
|
+
def handle_not_enough_precision
|
709
|
+
'AXAPI said there was not enough precision ¯\(°_o)/¯'
|
710
|
+
end
|
711
|
+
|
712
|
+
# @endgroup
|
713
|
+
|
714
|
+
|
715
|
+
##
|
716
|
+
# @private
|
717
|
+
#
|
718
|
+
# `Pointer` type encoding for `CFArrayRef` objects.
|
719
|
+
#
|
720
|
+
# @return [String]
|
721
|
+
ARRAY = '^{__CFArray}'
|
722
|
+
|
723
|
+
##
|
724
|
+
# @private
|
725
|
+
#
|
726
|
+
# `Pointer` type encoding for `AXUIElementRef` objects.
|
727
|
+
#
|
728
|
+
# @return [String]
|
729
|
+
ELEMENT = '^{__AXUIElement}'
|
730
|
+
|
731
|
+
##
|
732
|
+
# @private
|
733
|
+
#
|
734
|
+
# `Pointer` type encoding for `AXObserverRef` objects.
|
735
|
+
#
|
736
|
+
# @return [String]
|
737
|
+
OBSERVER = '^{__AXObserver}'
|
738
|
+
|
739
|
+
##
|
740
|
+
# @private
|
741
|
+
#
|
742
|
+
# Mapping of `AXError` values to static information on how to handle
|
743
|
+
# the error. Used by {handle_error}.
|
744
|
+
#
|
745
|
+
# @return [Hash{Number=>Array(Symbol,Range)}]
|
746
|
+
AXERROR = {
|
747
|
+
KAXErrorFailure => [RuntimeError, :handle_failure ],
|
748
|
+
KAXErrorIllegalArgument => [ArgumentError, :handle_illegal_argument ],
|
749
|
+
KAXErrorInvalidUIElement => [ArgumentError, :handle_invalid_element ],
|
750
|
+
KAXErrorInvalidUIElementObserver => [ArgumentError, :handle_invalid_observer ],
|
751
|
+
KAXErrorCannotComplete => [RuntimeError, :handle_cannot_complete ],
|
752
|
+
KAXErrorAttributeUnsupported => [ArgumentError, :handle_attr_unsupported ],
|
753
|
+
KAXErrorActionUnsupported => [ArgumentError, :handle_action_unsupported ],
|
754
|
+
KAXErrorNotificationUnsupported => [ArgumentError, :handle_notif_unsupported ],
|
755
|
+
KAXErrorNotImplemented => [NotImplementedError, :handle_not_implemented ],
|
756
|
+
KAXErrorNotificationAlreadyRegistered => [ArgumentError, :handle_notif_registered ],
|
757
|
+
KAXErrorNotificationNotRegistered => [RuntimeError, :handle_notif_not_registered ],
|
758
|
+
KAXErrorAPIDisabled => [RuntimeError, :handle_api_disabled ],
|
759
|
+
KAXErrorParameterizedAttributeUnsupported => [ArgumentError, :handle_param_attr_unsupported],
|
760
|
+
KAXErrorNotEnoughPrecision => [RuntimeError, :handle_not_enough_precision ]
|
761
|
+
}
|
762
|
+
end
|
763
|
+
|
764
|
+
|
765
|
+
##
|
766
|
+
# Mixin for the special `__NSCFType` class so that `#to_ruby` works properly.
|
767
|
+
module Accessibility::ValueUnwrapper
|
768
|
+
##
|
769
|
+
# Map of type encodings used for wrapping structs when coming from
|
770
|
+
# an `AXValueRef`.
|
771
|
+
#
|
772
|
+
# The list is order sensitive, which is why we unshift nil, but
|
773
|
+
# should probably be more rigorously defined at runtime.
|
774
|
+
#
|
775
|
+
# @return [String,nil]
|
776
|
+
BOX_TYPES = [CGPoint, CGSize, CGRect, CFRange].map!(&:type).unshift(nil)
|
777
|
+
|
778
|
+
##
|
779
|
+
# Unwrap an `AXValue` into the `Boxed` instance that it is supposed
|
780
|
+
# to be. This will only work for the most common boxed types, you will
|
781
|
+
# need to check the AXAPI documentation for an up to date list.
|
782
|
+
#
|
783
|
+
# @example
|
784
|
+
#
|
785
|
+
# wrapped_point.to_ruby # => #<CGPoint x=44.3 y=99.0>
|
786
|
+
# wrapped_range.to_ruby # => #<CFRange begin=7 length=100>
|
787
|
+
# wrapped_thing.to_ruby # => wrapped_thing
|
788
|
+
#
|
789
|
+
# @return [Boxed]
|
790
|
+
def to_ruby
|
791
|
+
box_type = AXValueGetType(self)
|
792
|
+
return self if box_type.zero?
|
793
|
+
ptr = Pointer.new BOX_TYPES[box_type]
|
794
|
+
AXValueGetValue(self, box_type, ptr)
|
795
|
+
ptr.value.to_ruby
|
796
|
+
end
|
797
|
+
end
|
798
|
+
|
799
|
+
# hack to find the proper class
|
800
|
+
klass = AXUIElementCreateSystemWide().class
|
801
|
+
klass.send :include, Accessibility::Core
|
802
|
+
klass.send :include, Accessibility::ValueUnwrapper
|
803
|
+
|
804
|
+
|
805
|
+
##
|
806
|
+
# AXElements extensions to the `Boxed` class. The `Boxed` class is
|
807
|
+
# simply an abstract base class for structs that MacRuby can use
|
808
|
+
# via bridge support.
|
809
|
+
class Boxed
|
810
|
+
##
|
811
|
+
# Returns the number that AXAPI uses in order to know how to wrap
|
812
|
+
# a struct.
|
813
|
+
#
|
814
|
+
# @return [Number]
|
815
|
+
def self.ax_value
|
816
|
+
raise NotImplementedError, "#{self.class} cannot be wraped"
|
817
|
+
end
|
818
|
+
|
819
|
+
##
|
820
|
+
# Create an `AXValueRef` from the `Boxed` instance. This will only
|
821
|
+
# work if for the most common boxed types, you will need to check
|
822
|
+
# the AXAPI documentation for an up to date list.
|
823
|
+
#
|
824
|
+
# @example
|
825
|
+
#
|
826
|
+
# CGPointMake(12, 34).to_ax # => #<AXValueRef:0x455678e2>
|
827
|
+
# CGSizeMake(56, 78).to_ax # => #<AXValueRef:0x555678e2>
|
828
|
+
#
|
829
|
+
# @return [AXValueRef]
|
830
|
+
def to_ax
|
831
|
+
klass = self.class
|
832
|
+
ptr = Pointer.new klass.type
|
833
|
+
ptr.assign self
|
834
|
+
AXValueCreate(klass.ax_value, ptr)
|
835
|
+
end
|
836
|
+
end
|
837
|
+
|
838
|
+
# AXElements extensions for `CFRange`.
|
839
|
+
class << CFRange; def ax_value; KAXValueCFRangeType; end end
|
840
|
+
# AXElements extensions for `CGSize`.
|
841
|
+
class << CGSize; def ax_value; KAXValueCGSizeType; end end
|
842
|
+
# AXElements extensions for `CGRect`.
|
843
|
+
class << CGRect; def ax_value; KAXValueCGRectType; end end
|
844
|
+
# AXElements extensions for `CGPoint`.
|
845
|
+
class << CGPoint; def ax_value; KAXValueCGPointType; end end
|
846
|
+
|
847
|
+
|
848
|
+
# AXElements extensions for `NSObject`.
|
849
|
+
class NSObject
|
850
|
+
def to_ax; self end
|
851
|
+
def to_ruby; self end
|
852
|
+
end
|
853
|
+
|
854
|
+
# AXElements extensions for `Range`.
|
855
|
+
class Range
|
856
|
+
# @return [AXValueRef]
|
857
|
+
def to_ax
|
858
|
+
raise ArgumentError if last < 0 || first < 0
|
859
|
+
length = if exclude_end?
|
860
|
+
last - first
|
861
|
+
else
|
862
|
+
last - first + 1
|
863
|
+
end
|
864
|
+
CFRange.new(first, length).to_ax
|
865
|
+
end
|
866
|
+
end
|
867
|
+
|
868
|
+
# AXElements extensions for `CFRange`.
|
869
|
+
class CFRange
|
870
|
+
# @return [Range]
|
871
|
+
def to_ruby
|
872
|
+
Range.new location, (location + length - 1)
|
873
|
+
end
|
874
|
+
end
|
875
|
+
|
876
|
+
|
877
|
+
# AXElements extensions to `NSArray`.
|
878
|
+
class NSArray
|
879
|
+
# @return [CGPoint]
|
880
|
+
def to_point; CGPoint.new(first, at(1)) end
|
881
|
+
# @return [CGSize]
|
882
|
+
def to_size; CGSize.new(first, at(1)) end
|
883
|
+
# @return [CGRect]
|
884
|
+
def to_rect; CGRectMake(*self[0..3]) end
|
885
|
+
end
|
886
|
+
|
887
|
+
# AXElements extensions for `CGPoint`.
|
888
|
+
class CGPoint
|
889
|
+
# @return [CGPoint]
|
890
|
+
def to_point; self end
|
891
|
+
end
|
892
|
+
|
893
|
+
##
|
894
|
+
# Cached reference to the system wide object.
|
895
|
+
#
|
896
|
+
# @return [AXUIElementRef]
|
897
|
+
SYSTEMWIDE = AXUIElementCreateSystemWide()
|