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