AXElements 0.6.0beta2 → 0.7.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. data/.yardopts +1 -2
  2. data/README.markdown +152 -88
  3. data/Rakefile +8 -103
  4. data/docs/Debugging.markdown +9 -2
  5. data/docs/KeyboardEvents.markdown +114 -49
  6. data/docs/Setting.markdown +1 -0
  7. data/docs/images/next_version.png +0 -0
  8. data/ext/accessibility/key_coder/extconf.rb +22 -0
  9. data/ext/accessibility/key_coder/key_coder.c +113 -0
  10. data/lib/AXElements.rb +2 -0
  11. data/lib/accessibility/core.rb +897 -0
  12. data/lib/accessibility/debug.rb +168 -0
  13. data/lib/accessibility/dsl.rb +697 -0
  14. data/lib/accessibility/enumerators.rb +104 -0
  15. data/lib/accessibility/errors.rb +32 -0
  16. data/lib/accessibility/factory.rb +153 -0
  17. data/lib/accessibility/graph.rb +150 -0
  18. data/lib/{ax_elements/inspector.rb → accessibility/pp_inspector.rb} +39 -28
  19. data/lib/accessibility/qualifier.rb +158 -0
  20. data/lib/accessibility/string.rb +494 -0
  21. data/lib/accessibility/translator.rb +178 -0
  22. data/lib/accessibility/version.rb +7 -0
  23. data/lib/accessibility.rb +79 -0
  24. data/lib/ax/application.rb +234 -0
  25. data/lib/{ax_elements/elements → ax}/button.rb +2 -0
  26. data/lib/ax/element.rb +518 -0
  27. data/lib/{ax_elements/elements → ax}/radio_button.rb +2 -0
  28. data/lib/ax/row.rb +37 -0
  29. data/lib/{ax_elements/elements → ax}/static_text.rb +2 -0
  30. data/lib/ax/systemwide.rb +86 -0
  31. data/lib/ax_elements/awesome_print.rb +25 -0
  32. data/lib/ax_elements/exception_workaround.rb +8 -0
  33. data/lib/ax_elements/nsarray_compat.rb +64 -0
  34. data/lib/ax_elements/vendor/inflection_data.rb +65 -0
  35. data/lib/ax_elements/vendor/inflections.rb +172 -0
  36. data/lib/ax_elements/vendor/inflector.rb +306 -0
  37. data/lib/ax_elements.rb +14 -25
  38. data/lib/minitest/ax_elements.rb +112 -12
  39. data/lib/mouse.rb +72 -46
  40. data/lib/rspec/expectations/ax_elements.rb +133 -6
  41. data/rakelib/doc.rake +13 -0
  42. data/rakelib/ext.rake +61 -0
  43. data/rakelib/gem.rake +28 -0
  44. data/rakelib/test.rake +53 -0
  45. data/test/helper.rb +11 -97
  46. data/test/integration/accessibility/test_core.rb +18 -0
  47. data/test/integration/accessibility/test_debug.rb +44 -0
  48. data/test/integration/accessibility/test_dsl.rb +225 -0
  49. data/test/integration/accessibility/test_enumerators.rb +122 -0
  50. data/test/integration/accessibility/test_errors.rb +38 -0
  51. data/test/integration/accessibility/test_notifications.rb +22 -0
  52. data/test/integration/accessibility/test_qualifier.rb +148 -0
  53. data/test/integration/ax/test_application.rb +56 -0
  54. data/test/integration/ax/test_element.rb +46 -0
  55. data/test/integration/ax/test_row.rb +23 -0
  56. data/test/integration/ax_elements/test_nsarray_compat.rb +43 -0
  57. data/test/integration/minitest/test_ax_elements.rb +98 -0
  58. data/test/integration/rspec/expectations/test_ax_elements.rb +58 -0
  59. data/test/integration/test_mouse.rb +35 -0
  60. data/test/sanity/accessibility/test_core.rb +553 -0
  61. data/test/sanity/accessibility/test_debug.rb +63 -0
  62. data/test/sanity/accessibility/test_dsl.rb +75 -0
  63. data/test/sanity/accessibility/test_errors.rb +10 -0
  64. data/test/sanity/accessibility/test_factory.rb +88 -0
  65. data/test/sanity/accessibility/test_pp_inspector.rb +110 -0
  66. data/test/sanity/accessibility/test_qualifier.rb +13 -0
  67. data/test/sanity/accessibility/test_string.rb +238 -0
  68. data/test/sanity/accessibility/test_translator.rb +145 -0
  69. data/test/sanity/ax/test_application.rb +90 -0
  70. data/test/sanity/ax/test_element.rb +80 -0
  71. data/test/sanity/ax/test_systemwide.rb +66 -0
  72. data/test/sanity/ax_elements/test_nsarray_compat.rb +16 -0
  73. data/test/sanity/ax_elements/test_nsobject_inspect.rb +11 -0
  74. data/test/sanity/minitest/test_ax_elements.rb +15 -0
  75. data/test/sanity/rspec/expectations/test_ax_elements.rb +12 -0
  76. data/test/sanity/test_ax_elements.rb +10 -0
  77. data/test/sanity/test_mouse.rb +19 -0
  78. metadata +111 -93
  79. data/LICENSE.txt +0 -25
  80. data/ext/key_coder/extconf.rb +0 -6
  81. data/ext/key_coder/key_coder.m +0 -77
  82. data/lib/ax_elements/accessibility/enumerators.rb +0 -104
  83. data/lib/ax_elements/accessibility/graph.rb +0 -118
  84. data/lib/ax_elements/accessibility/language.rb +0 -347
  85. data/lib/ax_elements/accessibility/qualifier.rb +0 -73
  86. data/lib/ax_elements/accessibility.rb +0 -166
  87. data/lib/ax_elements/core.rb +0 -541
  88. data/lib/ax_elements/element.rb +0 -593
  89. data/lib/ax_elements/elements/application.rb +0 -88
  90. data/lib/ax_elements/elements/row.rb +0 -30
  91. data/lib/ax_elements/elements/systemwide.rb +0 -46
  92. data/lib/ax_elements/macruby_extensions.rb +0 -255
  93. data/lib/ax_elements/notification.rb +0 -37
  94. data/lib/ax_elements/version.rb +0 -9
  95. data/test/elements/test_application.rb +0 -72
  96. data/test/elements/test_row.rb +0 -27
  97. data/test/elements/test_systemwide.rb +0 -38
  98. data/test/test_accessibility.rb +0 -127
  99. data/test/test_blankness.rb +0 -26
  100. data/test/test_core.rb +0 -448
  101. data/test/test_element.rb +0 -939
  102. data/test/test_enumerators.rb +0 -81
  103. data/test/test_inspector.rb +0 -130
  104. data/test/test_language.rb +0 -157
  105. data/test/test_macruby_extensions.rb +0 -303
  106. data/test/test_mouse.rb +0 -5
  107. data/test/test_search_semantics.rb +0 -143
@@ -1,166 +0,0 @@
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
- dot = Graph.new(root)
35
- dot.build!
36
- dot.to_s
37
- end
38
-
39
- ##
40
- # Dump a tree to the console, indenting for each level down the
41
- # tree that we go, and inspecting each element.
42
- #
43
- # @example
44
- #
45
- # puts Accessibility.dump(app)
46
- #
47
- # @return [String]
48
- def dump element
49
- output = element.inspect + "\n"
50
- enum = Accessibility::DFEnumerator.new(element)
51
- enum.each_with_height do |element, depth|
52
- output << "\t"*depth + element.inspect + "\n"
53
- end
54
- output
55
- end
56
-
57
- # @group Finding an object at a point
58
-
59
- ##
60
- # Get the current mouse position and return the top most element at
61
- # that point.
62
- #
63
- # @return [AX::Element]
64
- def element_under_mouse
65
- element_at_point Mouse.current_position
66
- end
67
-
68
- ##
69
- # Get the top most object at an arbitrary point on the screen.
70
- #
71
- # @overload element_at_point(x,y)
72
- # @param [Float] x
73
- # @param [Float] y
74
- #
75
- # @overload element_at_point([x,y])
76
- # @param [Array(Float,Float)] point
77
- #
78
- # @overload element_at_point(CGPoint.new(x,y))
79
- # @param [CGPoint] point
80
- #
81
- # @return [AX::Element]
82
- def element_at_point *point
83
- arg = point.size == 1 ? point.first : point
84
- AX::Element.process AX.element_at_point(*arg.to_a.flatten)
85
- end
86
- alias_method :element_at_position, :element_at_point
87
-
88
- # @group Finding an application object
89
-
90
- ##
91
- # @todo Find a way for this method to work without sleeping;
92
- # consider looping begin/rescue/end until AX starts up
93
- # @todo This needs to handle bad bundle identifier's gracefully
94
- #
95
- # This is the standard way of creating an application object. It will
96
- # launch the app if it is not already running and then create the
97
- # accessibility object.
98
- #
99
- # However, this method is a HUGE hack in cases where the app is not
100
- # already running; I've tried to register for notifications, launch
101
- # synchronously, etc., but there is always a problem with accessibility
102
- # not being ready. Hopefully this problem will go away on Lion...
103
- #
104
- # If this method fails to find an app with the appropriate bundle
105
- # identifier then it will return nil, eventually.
106
- #
107
- # @param [String] bundle a bundle identifier
108
- # @param [Float] sleep_time how long to wait between polling
109
- # @return [AX::Application,nil]
110
- def application_with_bundle_identifier bundle, sleep_time = 2
111
- sleep_count = 0
112
- while (apps = NSRunningApplication.runningApplicationsWithBundleIdentifier bundle).empty?
113
- launch_application bundle
114
- return if sleep_count > 10
115
- sleep sleep_time
116
- sleep_count += 1
117
- end
118
- application_with_pid apps.first.processIdentifier
119
- end
120
-
121
- ##
122
- # @todo We don't launch apps if they are not running, but we could if
123
- # we used `NSWorkspace#launchApplication`, but it will be a headache
124
- #
125
- # Get the accessibility object for an application given its localized
126
- # name. This will not work if the application is not already running.
127
- #
128
- # @param [String] name name of the application to launch
129
- # @return [AX::Application,nil]
130
- def application_with_name name
131
- NSRunLoop.currentRunLoop.runUntilDate Time.now
132
- workspace = NSWorkspace.sharedWorkspace
133
- app = workspace.runningApplications.find { |app| app.localizedName == name }
134
- application_with_pid(app.processIdentifier) if app
135
- end
136
-
137
- ##
138
- # Get the accessibility object for an application given its PID.
139
- #
140
- # @return [AX::Application]
141
- def application_with_pid pid
142
- AX::Element.process AX.application_for_pid pid
143
- end
144
-
145
- # @endgroup
146
-
147
-
148
- private
149
-
150
- ##
151
- # This method uses asynchronous method calls to launch applications.
152
- #
153
- # @param [String] bundle the bundle identifier for the app
154
- # @return [Boolean]
155
- def launch_application bundle
156
- NSWorkspace.sharedWorkspace.launchAppWithBundleIdentifier bundle,
157
- options: NSWorkspaceLaunchAsync,
158
- additionalEventParamDescriptor: nil,
159
- launchIdentifier: nil
160
- end
161
-
162
- end
163
-
164
-
165
- require 'ax_elements/accessibility/enumerators'
166
- require 'ax_elements/accessibility/qualifier'
@@ -1,541 +0,0 @@
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