AXElements 0.8.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/.yardopts +4 -0
  2. data/History.markdown +41 -0
  3. data/README.markdown +59 -62
  4. data/Rakefile +1 -1
  5. data/ext/accessibility/key_coder/extconf.rb +1 -1
  6. data/ext/accessibility/key_coder/key_coder.c +8 -5
  7. data/lib/accessibility/dsl.rb +261 -87
  8. data/lib/accessibility/enumerators.rb +14 -11
  9. data/lib/accessibility/errors.rb +4 -3
  10. data/lib/accessibility/factory.rb +159 -108
  11. data/lib/accessibility/graph.rb +13 -9
  12. data/lib/accessibility/{pp_inspector.rb → pretty_printer.rb} +4 -5
  13. data/lib/accessibility/qualifier.rb +23 -13
  14. data/lib/accessibility/string.rb +4 -4
  15. data/lib/accessibility/system_info.rb +230 -0
  16. data/lib/accessibility/translator.rb +38 -28
  17. data/lib/accessibility/version.rb +24 -2
  18. data/lib/accessibility.rb +25 -8
  19. data/lib/ax/application.rb +207 -77
  20. data/lib/ax/element.rb +62 -65
  21. data/lib/ax/menu.rb +5 -1
  22. data/lib/ax/row.rb +1 -1
  23. data/lib/ax/scroll_area.rb +7 -6
  24. data/lib/ax/systemwide.rb +38 -5
  25. data/lib/ax_elements/active_support_selections.rb +10 -0
  26. data/lib/ax_elements/mri.rb +57 -0
  27. data/lib/ax_elements/nsarray_compat.rb +97 -17
  28. data/lib/ax_elements.rb +9 -1
  29. data/rakelib/gem.rake +11 -11
  30. data/rakelib/test.rake +0 -9
  31. data/test/helper.rb +10 -18
  32. data/test/integration/accessibility/test_dsl.rb +52 -42
  33. data/test/integration/accessibility/test_enumerators.rb +0 -1
  34. data/test/integration/accessibility/test_graph.rb +1 -0
  35. data/test/integration/accessibility/test_qualifier.rb +2 -2
  36. data/test/integration/ax/test_application.rb +9 -2
  37. data/test/integration/ax/test_element.rb +41 -1
  38. data/test/sanity/accessibility/test_factory.rb +23 -56
  39. data/test/sanity/accessibility/{test_pp_inspector.rb → test_pretty_printer.rb} +9 -9
  40. data/test/sanity/accessibility/test_translator.rb +2 -5
  41. data/test/sanity/accessibility/test_version.rb +15 -0
  42. data/test/sanity/ax/test_application.rb +17 -2
  43. data/test/sanity/ax/test_element.rb +2 -2
  44. data/test/sanity/ax_elements/test_nsobject_inspect.rb +4 -2
  45. data/test/sanity/test_ax_elements.rb +1 -0
  46. metadata +69 -39
  47. data/lib/accessibility/core.rb +0 -973
  48. data/lib/accessibility/highlighter.rb +0 -86
  49. data/lib/ax_elements/vendor/inflection_data.rb +0 -66
  50. data/lib/ax_elements/vendor/inflections.rb +0 -172
  51. data/lib/ax_elements/vendor/inflector.rb +0 -306
  52. data/lib/minitest/ax_elements.rb +0 -175
  53. data/lib/mouse.rb +0 -223
  54. data/lib/rspec/expectations/ax_elements.rb +0 -234
  55. data/test/integration/accessibility/test_core.rb +0 -18
  56. data/test/integration/minitest/test_ax_elements.rb +0 -89
  57. data/test/integration/rspec/expectations/test_ax_elements.rb +0 -102
  58. data/test/sanity/accessibility/test_core.rb +0 -561
  59. data/test/sanity/accessibility/test_highlighter.rb +0 -56
  60. data/test/sanity/minitest/test_ax_elements.rb +0 -17
  61. data/test/sanity/rspec/expectations/test_ax_elements.rb +0 -15
  62. data/test/sanity/test_mouse.rb +0 -19
@@ -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