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.
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