AXElements 0.6.0beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/.yardopts +20 -0
  2. data/LICENSE.txt +25 -0
  3. data/README.markdown +150 -0
  4. data/Rakefile +109 -0
  5. data/docs/AccessibilityTips.markdown +119 -0
  6. data/docs/Acting.markdown +340 -0
  7. data/docs/Debugging.markdown +326 -0
  8. data/docs/Inspecting.markdown +255 -0
  9. data/docs/KeyboardEvents.markdown +57 -0
  10. data/docs/NewBehaviour.markdown +151 -0
  11. data/docs/Notifications.markdown +271 -0
  12. data/docs/Searching.markdown +250 -0
  13. data/docs/TestingExtensions.markdown +52 -0
  14. data/docs/images/AX.png +0 -0
  15. data/docs/images/all_the_buttons.jpg +0 -0
  16. data/docs/images/ui_hierarchy.dot +34 -0
  17. data/docs/images/ui_hierarchy.png +0 -0
  18. data/ext/key_coder/extconf.rb +6 -0
  19. data/ext/key_coder/key_coder.m +77 -0
  20. data/lib/ax_elements/accessibility/enumerators.rb +104 -0
  21. data/lib/ax_elements/accessibility/language.rb +347 -0
  22. data/lib/ax_elements/accessibility/qualifier.rb +73 -0
  23. data/lib/ax_elements/accessibility.rb +164 -0
  24. data/lib/ax_elements/core.rb +541 -0
  25. data/lib/ax_elements/element.rb +593 -0
  26. data/lib/ax_elements/elements/application.rb +88 -0
  27. data/lib/ax_elements/elements/button.rb +18 -0
  28. data/lib/ax_elements/elements/radio_button.rb +18 -0
  29. data/lib/ax_elements/elements/row.rb +30 -0
  30. data/lib/ax_elements/elements/static_text.rb +17 -0
  31. data/lib/ax_elements/elements/systemwide.rb +46 -0
  32. data/lib/ax_elements/inspector.rb +116 -0
  33. data/lib/ax_elements/macruby_extensions.rb +255 -0
  34. data/lib/ax_elements/notification.rb +37 -0
  35. data/lib/ax_elements/version.rb +9 -0
  36. data/lib/ax_elements.rb +30 -0
  37. data/lib/minitest/ax_elements.rb +19 -0
  38. data/lib/mouse.rb +185 -0
  39. data/lib/rspec/expectations/ax_elements.rb +15 -0
  40. data/test/elements/test_application.rb +72 -0
  41. data/test/elements/test_row.rb +27 -0
  42. data/test/elements/test_systemwide.rb +38 -0
  43. data/test/helper.rb +119 -0
  44. data/test/test_accessibility.rb +127 -0
  45. data/test/test_blankness.rb +26 -0
  46. data/test/test_core.rb +448 -0
  47. data/test/test_element.rb +939 -0
  48. data/test/test_enumerators.rb +81 -0
  49. data/test/test_inspector.rb +121 -0
  50. data/test/test_language.rb +157 -0
  51. data/test/test_macruby_extensions.rb +303 -0
  52. data/test/test_mouse.rb +5 -0
  53. data/test/test_search_semantics.rb +143 -0
  54. metadata +219 -0
@@ -0,0 +1,164 @@
1
+ require 'mouse'
2
+
3
+ ##
4
+ # The module that contains helpers for working with the accessibility APIs.
5
+ module Accessibility; end
6
+
7
+ class << Accessibility
8
+
9
+ # @group Debug helpers
10
+
11
+ ##
12
+ # Get a list of elements, starting with an element you give, and riding
13
+ # the hierarchy up to the top level object (i.e. the {AX::Application}).
14
+ #
15
+ # @example
16
+ #
17
+ # element = AX::DOCK.list.application_dock_item
18
+ # Accessibility.path(element) # => [AX::ApplicationDockItem, AX::List, AX::Application]
19
+ #
20
+ # @param [AX::Element]
21
+ # @return [Array<AX::Element>] the path in ascending order
22
+ def path *elements
23
+ element = elements.last
24
+ return path(elements << element.parent) if element.respond_to? :parent
25
+ return elements
26
+ end
27
+
28
+ ##
29
+ # Make a `dot` format graph of the tree, meant for graphing with
30
+ # GraphViz.
31
+ #
32
+ # @return [String]
33
+ def graph root
34
+ raise NotImplementedError, 'Please implement me, :('
35
+ end
36
+
37
+ ##
38
+ # Dump a tree to the console, indenting for each level down the
39
+ # tree that we go, and inspecting each element.
40
+ #
41
+ # @example
42
+ #
43
+ # puts Accessibility.dump(app)
44
+ #
45
+ # @return [String]
46
+ def dump element
47
+ output = element.inspect + "\n"
48
+ enum = Accessibility::DFEnumerator.new(element)
49
+ enum.each_with_height do |element, depth|
50
+ output << "\t"*depth + element.inspect + "\n"
51
+ end
52
+ output
53
+ end
54
+
55
+ # @group Finding an object at a point
56
+
57
+ ##
58
+ # Get the current mouse position and return the top most element at
59
+ # that point.
60
+ #
61
+ # @return [AX::Element]
62
+ def element_under_mouse
63
+ element_at_point Mouse.current_position
64
+ end
65
+
66
+ ##
67
+ # Get the top most object at an arbitrary point on the screen.
68
+ #
69
+ # @overload element_at_point(x,y)
70
+ # @param [Float] x
71
+ # @param [Float] y
72
+ #
73
+ # @overload element_at_point([x,y])
74
+ # @param [Array(Float,Float)] point
75
+ #
76
+ # @overload element_at_point(CGPoint.new(x,y))
77
+ # @param [CGPoint] point
78
+ #
79
+ # @return [AX::Element]
80
+ def element_at_point *point
81
+ arg = point.size == 1 ? point.first : point
82
+ AX::Element.process AX.element_at_point(*arg.to_a.flatten)
83
+ end
84
+ alias_method :element_at_position, :element_at_point
85
+
86
+ # @group Finding an application object
87
+
88
+ ##
89
+ # @todo Find a way for this method to work without sleeping;
90
+ # consider looping begin/rescue/end until AX starts up
91
+ # @todo This needs to handle bad bundle identifier's gracefully
92
+ #
93
+ # This is the standard way of creating an application object. It will
94
+ # launch the app if it is not already running and then create the
95
+ # accessibility object.
96
+ #
97
+ # However, this method is a HUGE hack in cases where the app is not
98
+ # already running; I've tried to register for notifications, launch
99
+ # synchronously, etc., but there is always a problem with accessibility
100
+ # not being ready. Hopefully this problem will go away on Lion...
101
+ #
102
+ # If this method fails to find an app with the appropriate bundle
103
+ # identifier then it will return nil, eventually.
104
+ #
105
+ # @param [String] bundle a bundle identifier
106
+ # @param [Float] sleep_time how long to wait between polling
107
+ # @return [AX::Application,nil]
108
+ def application_with_bundle_identifier bundle, sleep_time = 2
109
+ sleep_count = 0
110
+ while (apps = NSRunningApplication.runningApplicationsWithBundleIdentifier bundle).empty?
111
+ launch_application bundle
112
+ return if sleep_count > 10
113
+ sleep sleep_time
114
+ sleep_count += 1
115
+ end
116
+ application_with_pid apps.first.processIdentifier
117
+ end
118
+
119
+ ##
120
+ # @todo We don't launch apps if they are not running, but we could if
121
+ # we used `NSWorkspace#launchApplication`, but it will be a headache
122
+ #
123
+ # Get the accessibility object for an application given its localized
124
+ # name. This will not work if the application is not already running.
125
+ #
126
+ # @param [String] name name of the application to launch
127
+ # @return [AX::Application,nil]
128
+ def application_with_name name
129
+ NSRunLoop.currentRunLoop.runUntilDate Time.now
130
+ workspace = NSWorkspace.sharedWorkspace
131
+ app = workspace.runningApplications.find { |app| app.localizedName == name }
132
+ application_with_pid(app.processIdentifier) if app
133
+ end
134
+
135
+ ##
136
+ # Get the accessibility object for an application given its PID.
137
+ #
138
+ # @return [AX::Application]
139
+ def application_with_pid pid
140
+ AX::Element.process AX.application_for_pid pid
141
+ end
142
+
143
+ # @endgroup
144
+
145
+
146
+ private
147
+
148
+ ##
149
+ # This method uses asynchronous method calls to launch applications.
150
+ #
151
+ # @param [String] bundle the bundle identifier for the app
152
+ # @return [Boolean]
153
+ def launch_application bundle
154
+ NSWorkspace.sharedWorkspace.launchAppWithBundleIdentifier bundle,
155
+ options: NSWorkspaceLaunchAsync,
156
+ additionalEventParamDescriptor: nil,
157
+ launchIdentifier: nil
158
+ end
159
+
160
+ end
161
+
162
+
163
+ require 'ax_elements/accessibility/enumerators'
164
+ require 'ax_elements/accessibility/qualifier'
@@ -0,0 +1,541 @@
1
+ require 'logger'
2
+
3
+ module Accessibility
4
+ class << self
5
+ # @return [Logger]
6
+ attr_accessor :log
7
+ end
8
+
9
+ @log = Logger.new $stderr
10
+ @log.level = Logger::ERROR # @todo lame
11
+ end
12
+
13
+ ##
14
+ # Namespace for all the accessibility objects, as well as core
15
+ # abstraction layer that that interact with OS X Accessibility
16
+ # APIs.
17
+ module AX
18
+ @ignore_notifs = true
19
+ @notifs = {}
20
+ end
21
+
22
+ ##
23
+ # @todo The current strategy dealing with errors is just to log them,
24
+ # but that may not always be the correct thing to do. The core
25
+ # has to be refactored around this issue to become more robust.
26
+ # I've already started doing this for newer APIs but the old ones
27
+ # are important as well.
28
+ # @todo I feel a bit weird having to instantiate a new pointer every
29
+ # time I want to fetch an attribute. Since allocations are costly
30
+ # it hurts performance a lot when it comes to searches. I wonder if
31
+ # it would pay off to have a pool of pointers...
32
+ #
33
+ # The singleton methods for the AX module represent the core layer of
34
+ # abstraction for AXElements.
35
+ #
36
+ # The methods provide a clean Ruby-ish interface to the low level
37
+ # CoreFoundation functions that compose the AXAPI. Doing this we can
38
+ # hide away the need to work with pointers and centralize when errors
39
+ # are logged from the low level function calls (since CoreFoundation
40
+ # uses a different pattern for that sort of thing).
41
+ #
42
+ # Ideally this API would be stateless, but I'm still working on that...
43
+ class << AX
44
+
45
+ # @group Attributes
46
+
47
+ ##
48
+ # List of attributes for the given element.
49
+ #
50
+ # @param [AXUIElementRef] element low level accessibility object
51
+ # @return [Array<String>]
52
+ def attrs_of_element element
53
+ ptr = Pointer.new ARRAY
54
+ code = AXUIElementCopyAttributeNames(element, ptr)
55
+ log_error element, code unless code.zero?
56
+ ptr[0]
57
+ end
58
+
59
+ ##
60
+ # Number of elements that would be returned for the given element's
61
+ # given attribute.
62
+ #
63
+ # @param [AXUIElementRef]
64
+ # @param [String] attr an attribute constant
65
+ # @return [Fixnum]
66
+ def attr_count_of_element element, attr
67
+ ptr = Pointer.new :long_long
68
+ code = AXUIElementGetAttributeValueCount(element, attr, ptr)
69
+ log_error element, attr unless code.zero?
70
+ ptr[0]
71
+ end
72
+
73
+ ##
74
+ # Fetch the given attribute's value from the given element. You will
75
+ # be given raw data from this method; that is, {Boxed} objects will
76
+ # still be wrapped in a `AXValueRef`, and elements will be
77
+ # `AXUIElementRef` objects.
78
+ #
79
+ # @param [AXUIElementRef]
80
+ # @param [String] attr an attribute constant
81
+ def attr_of_element element, attr
82
+ ptr = Pointer.new :id
83
+ code = AXUIElementCopyAttributeValue(element, attr, ptr)
84
+ log_error element, code unless code.zero?
85
+ ptr[0]
86
+ end
87
+
88
+ ##
89
+ # @todo Should we handle cases where a subrole has a value of
90
+ # 'Unknown'? What is the performance impact?
91
+ #
92
+ # Fetch subrole and role of an object, pass back an array with the
93
+ # subrole first if it exists.
94
+ #
95
+ # @param [AXUIElementRef]
96
+ # @param [Array<String>]
97
+ # @return [Array<String>]
98
+ def role_for element, attrs
99
+ ptr = Pointer.new :id
100
+ AXUIElementCopyAttributeValue(element, ROLE, ptr)
101
+ ret = [ptr[0]]
102
+ if attrs.include? SUBROLE
103
+ AXUIElementCopyAttributeValue(element, SUBROLE, ptr)
104
+ # Be careful, some things claim to have a subrole but return nil
105
+ ret.unshift ptr[0] if ptr[0]
106
+ end
107
+ #raise "Found an element that has no role: #{CFShow(element)}"
108
+ ret
109
+ end
110
+
111
+ ##
112
+ # Ask whether or not the given attribute of a given element can be
113
+ # changed using the accessibility APIs.
114
+ #
115
+ # @param [AXUIElementRef]
116
+ # @param [String] attr an attribute constant
117
+ def attr_of_element_writable? element, attr
118
+ ptr = Pointer.new :bool
119
+ code = AXUIElementIsAttributeSettable(element, attr, ptr)
120
+ log_error element, code unless code.zero?
121
+ ptr[0]
122
+ end
123
+
124
+ ##
125
+ # @note This method does not check writability of the attribute
126
+ # you are setting.
127
+ #
128
+ # Set the given value to the given attribute of the given element.
129
+ #
130
+ # @param [AXUIElementRef] element
131
+ # @param [String] attr an attribute constant
132
+ # @param [Object] value the new value to set on the attribute
133
+ # @return [Object] returns the value that was set
134
+ def set_attr_of_element element, attr, value
135
+ code = AXUIElementSetAttributeValue(element, attr, value)
136
+ log_error element, code unless code.zero?
137
+ value
138
+ end
139
+
140
+ # @group Actions
141
+
142
+ ##
143
+ # List of actions that the given element can perform.
144
+ #
145
+ # @param [AXUIElementRef] element low level accessibility object
146
+ # @return [Array<String>]
147
+ def actions_of_element element
148
+ array_ptr = Pointer.new ARRAY
149
+ code = AXUIElementCopyActionNames(element, array_ptr)
150
+ log_error element, code unless code.zero?
151
+ array_ptr[0]
152
+ end
153
+
154
+ ##
155
+ # Trigger the given action for the given element.
156
+ #
157
+ # @param [AXUIElementRef] element
158
+ # @param [String] action an action constant
159
+ # @return [Boolean] true if successful
160
+ def action_of_element element, action
161
+ code = AXUIElementPerformAction(element, action)
162
+ code.zero? ? true : (log_error(element, code); false)
163
+ end
164
+
165
+ ##
166
+ # In cases where you need (or want) to simulate keyboard input, such as
167
+ # triggering hotkeys, you will need to use this method.
168
+ #
169
+ # See the documentation page on
170
+ # {file:docs/KeyboardEvents.markdown Keyboard Events}
171
+ # to get a detailed explanation on how to encode strings.
172
+ #
173
+ # @param [AXUIElementRef] element an application to post the event to, or
174
+ # the system wide accessibility object
175
+ # @param [String] string the string you want typed on the screen
176
+ def keyboard_action element, string
177
+ post_kb_events element, parse_kb_string(string)
178
+ nil
179
+ end
180
+
181
+ # @group Parameterized Attributes
182
+
183
+ ##
184
+ # List of parameterized attributes for the given element.
185
+ #
186
+ # @param [AXUIElementRef] element low level accessibility object
187
+ # @return [Array<String>]
188
+ def param_attrs_of_element element
189
+ array_ptr = Pointer.new ARRAY
190
+ code = AXUIElementCopyParameterizedAttributeNames(element, array_ptr)
191
+ log_error element, code unless code.zero?
192
+ array_ptr[0]
193
+ end
194
+
195
+ ##
196
+ # Fetch the given attribute's value from the given element using the given
197
+ # parameter. You will be given raw data from this method; that is, {Boxed}
198
+ # objects will still be wrapped in a `AXValueRef`, etc.
199
+ #
200
+ # @param [AXUIElementRef] element
201
+ # @param [String] attr an attribute constant
202
+ def param_attr_of_element element, attr, param
203
+ ptr = Pointer.new :id
204
+ code = AXUIElementCopyParameterizedAttributeValue(element, attr, param, ptr)
205
+ log_error element, code unless code.zero?
206
+ ptr[0]
207
+ end
208
+
209
+ # @group Notifications
210
+
211
+ ##
212
+ # @todo This method is too big, needs refactoring. It's own class?
213
+ #
214
+ # {file:docs/Notifications.markdown Notifications} are a way to put
215
+ # non-polling delays into your scripts.
216
+ #
217
+ # Use this method to register to be notified of the specified event in
218
+ # an application. You must also pass a block to this method to validate
219
+ # the notification.
220
+ #
221
+ # @param [AXUIElementRef] ref the element which will send the notification
222
+ # @param [String] name the name of the notification
223
+ # @yield Validate the notification; the block should return truthy if
224
+ # the notification received is the expected one and the script can stop
225
+ # waiting, otherwise should return falsy.
226
+ # @yieldparam [AXUIElementRef] element the element that sent the notification
227
+ # @yieldparam [String] notif the name of the notification
228
+ # @yieldreturn [Boolean] determines if the script should continue or wait
229
+ # @return [Array(Observer, AXUIElementRef, String)] the registration triple
230
+ def register_for_notif ref, name, &block
231
+ run_loop = CFRunLoopGetCurrent()
232
+
233
+ # we are ignoring the context pointer since this is OO
234
+ callback = Proc.new do |observer, element, notif, _|
235
+ LOCK.synchronize do
236
+ Accessibility.log.debug "Received notif (#{notif}) for (#{element})"
237
+ break if @ignore_notifs
238
+ break unless block.call(element, notif)
239
+
240
+ @ignore_notifs = true
241
+ source = AXObserverGetRunLoopSource(observer)
242
+ CFRunLoopRemoveSource(run_loop, source, KCFRunLoopDefaultMode)
243
+ unregister_notif_callback observer, element, notif
244
+ CFRunLoopStop(run_loop)
245
+ end
246
+ end
247
+
248
+ dude = make_observer_for ref, callback
249
+ source = AXObserverGetRunLoopSource(dude)
250
+ register_notif_callback dude, ref, name
251
+ CFRunLoopAddSource(run_loop, source, KCFRunLoopDefaultMode)
252
+ @ignore_notifs = false
253
+
254
+ # must keep [element, observer, notif] in order to do unregistration
255
+ @notifs[dude] = [ref, name]
256
+ [dude, ref, name]
257
+ end
258
+
259
+ ##
260
+ # @todo Is it safe to end the run loop when _any_ source is handled or
261
+ # should we continue to kill the run loop when the callback is
262
+ # received?
263
+ #
264
+ # Pause execution of the program until a notification is received or a
265
+ # timeout occurs.
266
+ #
267
+ # @param [Float] timeout
268
+ # @return [Boolean] true if the notification was received, otherwise false
269
+ def wait_for_notif timeout
270
+ # We use RunInMode because it has timeout functionality, return values are
271
+ case CFRunLoopRunInMode(KCFRunLoopDefaultMode, timeout, false)
272
+ when KCFRunLoopRunStopped # Stopped with CFRunLoopStop.
273
+ true
274
+ when KCFRunLoopRunTimedOut # Time interval seconds passed.
275
+ false
276
+ when KCFRunLoopFinished # Mode has no sources or timers.
277
+ raise 'Something went wrong with setting up the run loop'
278
+ when KCFRunLoopRunHandledSource
279
+ # Only applies when returnAfterSourceHandled is true.
280
+ raise 'This should never happen'
281
+ else
282
+ raise 'You just found a an OS X bug (or a MacRuby bug)...'
283
+ end
284
+ end
285
+
286
+ ##
287
+ # @todo Flush any waiting notifs?
288
+ #
289
+ # Cancel _all_ notification registrations. Simple and clean, but a
290
+ # blunt tool at best. I didn't have time to figure out a better
291
+ # system :(
292
+ #
293
+ # @return [nil]
294
+ def unregister_notifs
295
+ LOCK.synchronize do
296
+ @ignore_notifs = true
297
+ @notifs.each_pair do |observer, pair|
298
+ unregister_notif_callback observer, *pair
299
+ end
300
+ @notifs = {}
301
+ end
302
+ end
303
+
304
+ # @group Element Entry Points
305
+
306
+ ##
307
+ # This will give you the UI element located at the position given. If
308
+ # more than one element is at the position then the z-order of the
309
+ # elements will be used to determine which is "on top".
310
+ #
311
+ # The coordinates should be specified using the flipped coordinate
312
+ # system (origin is in the top-left, increasing downward as if reading
313
+ # a book in English).
314
+ #
315
+ # @param [Float]
316
+ # @param [Float]
317
+ # @return [AXUIElementRef]
318
+ def element_at_point x, y
319
+ ptr = Pointer.new ELEMENT
320
+ system = AXUIElementCreateSystemWide()
321
+ code = AXUIElementCopyElementAtPosition(system, x, y, ptr)
322
+ log_error system, code unless code.zero?
323
+ ptr[0]
324
+ end
325
+
326
+ ##
327
+ # You can call this method to create the application object given
328
+ # the process identifier of the app.
329
+ #
330
+ # @param [Fixnum] pid process identifier for the application you want
331
+ # @return [AXUIElementRef]
332
+ def application_for_pid pid
333
+ raise ArgumentError, 'pid must be greater than 0' unless pid > 0
334
+ AXUIElementCreateApplication(pid)
335
+ end
336
+
337
+ # @group Misc.
338
+
339
+ ##
340
+ # Get the PID of the application that the given element belongs to.
341
+ #
342
+ # @param [AXUIElementRef] element
343
+ # @return [Fixnum]
344
+ def pid_of_element element
345
+ ptr = Pointer.new :int
346
+ code = AXUIElementGetPid(element, ptr)
347
+ log_error element, code unless code.zero?
348
+ ptr[0]
349
+ end
350
+
351
+ # @endgroup
352
+
353
+
354
+ private
355
+
356
+ ##
357
+ # @private
358
+ #
359
+ # Pointer type encoding for `CFArrayRef` objects.
360
+ #
361
+ # @return [String]
362
+ ARRAY = '^{__CFArray}'.freeze
363
+
364
+ ##
365
+ # @private
366
+ #
367
+ # Pointer type encoding for `AXUIElementRef` objects.
368
+ #
369
+ # @return [String]
370
+ ELEMENT = '^{__AXUIElement}'.freeze
371
+
372
+ ##
373
+ # @private
374
+ #
375
+ # Pointer type encoding for `AXObserverRef` objects.
376
+ #
377
+ # @return [String]
378
+ OBSERVER = '^{__AXObserver}'.freeze
379
+
380
+ ##
381
+ # @private
382
+ #
383
+ # Local copy of a Cocoa constant; this is a performance hack.
384
+ #
385
+ # @return [String]
386
+ ROLE = KAXRoleAttribute
387
+
388
+ ##
389
+ # @private
390
+ #
391
+ # Local copy of a Cocoa constant; this is a performance hack.
392
+ #
393
+ # @return [String]
394
+ SUBROLE = KAXSubroleAttribute
395
+
396
+ # @group Notifications
397
+
398
+ ##
399
+ # @todo Would a Dispatch::Semaphore be better?
400
+ #
401
+ # Semaphore used to synchronize async notification stuff.
402
+ #
403
+ # @return [Mutex]
404
+ LOCK = Mutex.new
405
+
406
+ # @endgroup
407
+
408
+ ##
409
+ # Map of characters to keycodes. The map is generated at boot time in
410
+ # order to support multiple keyboard layouts.
411
+ #
412
+ # @return [Hash]
413
+ KEYCODE_MAP = {}
414
+ require 'key_coder'
415
+
416
+ ##
417
+ # Parse a string into a list of keyboard events to be executed in
418
+ # the given order.
419
+ #
420
+ # @param [String]
421
+ # @return [Array<Array(Number,Boolean)>]
422
+ def parse_kb_string string
423
+ sequence = []
424
+ string.each_char do |char|
425
+ if char.match(/[A-Z]/)
426
+ code = AX::KEYCODE_MAP[char.downcase]
427
+ event = [[56,true], [code,true], [code,false], [56,false]]
428
+ else
429
+ code = AX::KEYCODE_MAP[char]
430
+ event = [[code,true],[code,false]]
431
+ end
432
+ sequence.concat event
433
+ end
434
+ sequence
435
+ end
436
+
437
+ ##
438
+ # Post the list of given keyboard events to the given element.
439
+ #
440
+ # @param [AXUIElementRef] element must be an application or the
441
+ # system-wide object
442
+ # @param [Array<Array(Number,Boolean)>]
443
+ def post_kb_events element, events
444
+ events.each do |event|
445
+ code = AXUIElementPostKeyboardEvent(element, 0, *event)
446
+ log_error element, code unless code.zero?
447
+ end
448
+ end
449
+
450
+ ##
451
+ # @private
452
+ #
453
+ # A mapping of the `AXError` constants to human readable strings, though
454
+ # this has to be actively maintained in case of changes to Apple's
455
+ # documentation in the future.
456
+ #
457
+ # @return [Hash{Fixnum=>String}]
458
+ AXError = {
459
+ KAXErrorFailure => 'Generic Failure',
460
+ KAXErrorIllegalArgument => 'Illegal Argument',
461
+ KAXErrorInvalidUIElement => 'Invalid UI Element',
462
+ KAXErrorInvalidUIElementObserver => 'Invalid UI Element Observer',
463
+ KAXErrorCannotComplete => 'Cannot Complete',
464
+ KAXErrorAttributeUnsupported => 'Attribute Unsupported',
465
+ KAXErrorActionUnsupported => 'Action Unsupported',
466
+ KAXErrorNotificationUnsupported => 'Notification Unsupported',
467
+ KAXErrorNotImplemented => 'Not Implemented',
468
+ KAXErrorNotificationAlreadyRegistered => 'Notification Already Registered',
469
+ KAXErrorNotificationNotRegistered => 'Notification Not Registered',
470
+ KAXErrorAPIDisabled => 'API Disabled',
471
+ KAXErrorNoValue => 'No Value',
472
+ KAXErrorParameterizedAttributeUnsupported => 'Parameterized Attribute Unsupported',
473
+ KAXErrorNotEnoughPrecision => 'Not Enough Precision'
474
+ }
475
+
476
+ ##
477
+ # Uses the call stack and error code to log a message that might be
478
+ # helpful in debugging.
479
+ #
480
+ # @param [AXUIElementRef]
481
+ # @param [Fixnum] code AXError value
482
+ def log_error element, code
483
+ message = AXError[code] || 'UNKNOWN ERROR CODE'
484
+ logger = Accessibility.log
485
+ logger.warn "[#{message} (#{code})] while trying #{caller[0]}"
486
+ logger.info "Available attributes were:\n#{attrs_of_element(element)}"
487
+ logger.info "Available actions were:\n#{actions_of_element(element)}"
488
+ # @todo logger.info available parameterized attributes
489
+ logger.debug "Backtrace: #{caller.description}"
490
+ # @todo logger.debug pp hierarchy element or pp element
491
+ end
492
+
493
+ ##
494
+ # Create and return a notification observer for the given object's
495
+ # application.
496
+ #
497
+ # @param [AXUIElementRef] element
498
+ # @param [Method,Proc] callback
499
+ # @return [AXObserverRef]
500
+ def make_observer_for element, callback
501
+ ptr = Pointer.new OBSERVER
502
+ code = AXObserverCreate(pid_of_element(element), callback, ptr)
503
+ log_error element, code unless code.zero?
504
+ ptr[0]
505
+ end
506
+
507
+ ##
508
+ # @todo Consider exposing the refcon argument. Probably not until
509
+ # someone actually wants to pass a context around.
510
+ #
511
+ # Register an observer for a specific event.
512
+ #
513
+ # @param [AXObserverRef]
514
+ # @param [AX::Element]
515
+ # @param [String]
516
+ def register_notif_callback observer, element, notif
517
+ code = AXObserverAddNotification(observer, element, notif, nil)
518
+ log_error element, code unless code.zero?
519
+ end
520
+
521
+ ##
522
+ # @todo No need to capture error code when handling all error cases
523
+ # properly. So I should get around to that soon.
524
+ #
525
+ # Unregister a notification that has been previously setup.
526
+ #
527
+ # @param [AXObserverRef]
528
+ # @param [AX::Element]
529
+ # @param [String]
530
+ def unregister_notif_callback observer, ref, notif
531
+ case code = AXObserverRemoveNotification(observer, ref, notif)
532
+ when KAXErrorNotificationNotRegistered
533
+ Accessibility.log.warn "Notif no longer registered: (#{ref}:#{notif})"
534
+ when KAXErrorIllegalArgument
535
+ raise ArgumentError, "Notif not unregistered (#{ref}:#{notif})"
536
+ else
537
+ log_error element, code unless code.zero?
538
+ end
539
+ end
540
+
541
+ end