AXElements 0.6.0beta1

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