AXElements 0.6.0beta2 → 0.7.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. data/.yardopts +1 -2
  2. data/README.markdown +152 -88
  3. data/Rakefile +8 -103
  4. data/docs/Debugging.markdown +9 -2
  5. data/docs/KeyboardEvents.markdown +114 -49
  6. data/docs/Setting.markdown +1 -0
  7. data/docs/images/next_version.png +0 -0
  8. data/ext/accessibility/key_coder/extconf.rb +22 -0
  9. data/ext/accessibility/key_coder/key_coder.c +113 -0
  10. data/lib/AXElements.rb +2 -0
  11. data/lib/accessibility/core.rb +897 -0
  12. data/lib/accessibility/debug.rb +168 -0
  13. data/lib/accessibility/dsl.rb +697 -0
  14. data/lib/accessibility/enumerators.rb +104 -0
  15. data/lib/accessibility/errors.rb +32 -0
  16. data/lib/accessibility/factory.rb +153 -0
  17. data/lib/accessibility/graph.rb +150 -0
  18. data/lib/{ax_elements/inspector.rb → accessibility/pp_inspector.rb} +39 -28
  19. data/lib/accessibility/qualifier.rb +158 -0
  20. data/lib/accessibility/string.rb +494 -0
  21. data/lib/accessibility/translator.rb +178 -0
  22. data/lib/accessibility/version.rb +7 -0
  23. data/lib/accessibility.rb +79 -0
  24. data/lib/ax/application.rb +234 -0
  25. data/lib/{ax_elements/elements → ax}/button.rb +2 -0
  26. data/lib/ax/element.rb +518 -0
  27. data/lib/{ax_elements/elements → ax}/radio_button.rb +2 -0
  28. data/lib/ax/row.rb +37 -0
  29. data/lib/{ax_elements/elements → ax}/static_text.rb +2 -0
  30. data/lib/ax/systemwide.rb +86 -0
  31. data/lib/ax_elements/awesome_print.rb +25 -0
  32. data/lib/ax_elements/exception_workaround.rb +8 -0
  33. data/lib/ax_elements/nsarray_compat.rb +64 -0
  34. data/lib/ax_elements/vendor/inflection_data.rb +65 -0
  35. data/lib/ax_elements/vendor/inflections.rb +172 -0
  36. data/lib/ax_elements/vendor/inflector.rb +306 -0
  37. data/lib/ax_elements.rb +14 -25
  38. data/lib/minitest/ax_elements.rb +112 -12
  39. data/lib/mouse.rb +72 -46
  40. data/lib/rspec/expectations/ax_elements.rb +133 -6
  41. data/rakelib/doc.rake +13 -0
  42. data/rakelib/ext.rake +61 -0
  43. data/rakelib/gem.rake +28 -0
  44. data/rakelib/test.rake +53 -0
  45. data/test/helper.rb +11 -97
  46. data/test/integration/accessibility/test_core.rb +18 -0
  47. data/test/integration/accessibility/test_debug.rb +44 -0
  48. data/test/integration/accessibility/test_dsl.rb +225 -0
  49. data/test/integration/accessibility/test_enumerators.rb +122 -0
  50. data/test/integration/accessibility/test_errors.rb +38 -0
  51. data/test/integration/accessibility/test_notifications.rb +22 -0
  52. data/test/integration/accessibility/test_qualifier.rb +148 -0
  53. data/test/integration/ax/test_application.rb +56 -0
  54. data/test/integration/ax/test_element.rb +46 -0
  55. data/test/integration/ax/test_row.rb +23 -0
  56. data/test/integration/ax_elements/test_nsarray_compat.rb +43 -0
  57. data/test/integration/minitest/test_ax_elements.rb +98 -0
  58. data/test/integration/rspec/expectations/test_ax_elements.rb +58 -0
  59. data/test/integration/test_mouse.rb +35 -0
  60. data/test/sanity/accessibility/test_core.rb +553 -0
  61. data/test/sanity/accessibility/test_debug.rb +63 -0
  62. data/test/sanity/accessibility/test_dsl.rb +75 -0
  63. data/test/sanity/accessibility/test_errors.rb +10 -0
  64. data/test/sanity/accessibility/test_factory.rb +88 -0
  65. data/test/sanity/accessibility/test_pp_inspector.rb +110 -0
  66. data/test/sanity/accessibility/test_qualifier.rb +13 -0
  67. data/test/sanity/accessibility/test_string.rb +238 -0
  68. data/test/sanity/accessibility/test_translator.rb +145 -0
  69. data/test/sanity/ax/test_application.rb +90 -0
  70. data/test/sanity/ax/test_element.rb +80 -0
  71. data/test/sanity/ax/test_systemwide.rb +66 -0
  72. data/test/sanity/ax_elements/test_nsarray_compat.rb +16 -0
  73. data/test/sanity/ax_elements/test_nsobject_inspect.rb +11 -0
  74. data/test/sanity/minitest/test_ax_elements.rb +15 -0
  75. data/test/sanity/rspec/expectations/test_ax_elements.rb +12 -0
  76. data/test/sanity/test_ax_elements.rb +10 -0
  77. data/test/sanity/test_mouse.rb +19 -0
  78. metadata +111 -93
  79. data/LICENSE.txt +0 -25
  80. data/ext/key_coder/extconf.rb +0 -6
  81. data/ext/key_coder/key_coder.m +0 -77
  82. data/lib/ax_elements/accessibility/enumerators.rb +0 -104
  83. data/lib/ax_elements/accessibility/graph.rb +0 -118
  84. data/lib/ax_elements/accessibility/language.rb +0 -347
  85. data/lib/ax_elements/accessibility/qualifier.rb +0 -73
  86. data/lib/ax_elements/accessibility.rb +0 -166
  87. data/lib/ax_elements/core.rb +0 -541
  88. data/lib/ax_elements/element.rb +0 -593
  89. data/lib/ax_elements/elements/application.rb +0 -88
  90. data/lib/ax_elements/elements/row.rb +0 -30
  91. data/lib/ax_elements/elements/systemwide.rb +0 -46
  92. data/lib/ax_elements/macruby_extensions.rb +0 -255
  93. data/lib/ax_elements/notification.rb +0 -37
  94. data/lib/ax_elements/version.rb +0 -9
  95. data/test/elements/test_application.rb +0 -72
  96. data/test/elements/test_row.rb +0 -27
  97. data/test/elements/test_systemwide.rb +0 -38
  98. data/test/test_accessibility.rb +0 -127
  99. data/test/test_blankness.rb +0 -26
  100. data/test/test_core.rb +0 -448
  101. data/test/test_element.rb +0 -939
  102. data/test/test_enumerators.rb +0 -81
  103. data/test/test_inspector.rb +0 -130
  104. data/test/test_language.rb +0 -157
  105. data/test/test_macruby_extensions.rb +0 -303
  106. data/test/test_mouse.rb +0 -5
  107. 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()