AXElements 0.9.0 → 1.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. data/.yardopts +0 -4
  2. data/README.markdown +22 -17
  3. data/Rakefile +1 -1
  4. data/ext/accessibility/key_coder/extconf.rb +1 -1
  5. data/ext/accessibility/key_coder/key_coder.c +2 -4
  6. data/lib/accessibility.rb +3 -3
  7. data/lib/accessibility/core.rb +948 -0
  8. data/lib/accessibility/dsl.rb +30 -186
  9. data/lib/accessibility/enumerators.rb +1 -0
  10. data/lib/accessibility/factory.rb +78 -134
  11. data/lib/accessibility/graph.rb +5 -9
  12. data/lib/accessibility/highlighter.rb +86 -0
  13. data/lib/accessibility/{pretty_printer.rb → pp_inspector.rb} +4 -3
  14. data/lib/accessibility/qualifier.rb +3 -5
  15. data/lib/accessibility/screen_recorder.rb +217 -0
  16. data/lib/accessibility/statistics.rb +57 -0
  17. data/lib/accessibility/translator.rb +23 -32
  18. data/lib/accessibility/version.rb +2 -22
  19. data/lib/ax/application.rb +20 -159
  20. data/lib/ax/element.rb +42 -32
  21. data/lib/ax/scroll_area.rb +5 -6
  22. data/lib/ax/systemwide.rb +1 -33
  23. data/lib/ax_elements.rb +1 -9
  24. data/lib/ax_elements/core_graphics_workaround.rb +5 -0
  25. data/lib/ax_elements/nsarray_compat.rb +17 -97
  26. data/lib/ax_elements/vendor/inflection_data.rb +66 -0
  27. data/lib/ax_elements/vendor/inflections.rb +176 -0
  28. data/lib/ax_elements/vendor/inflector.rb +306 -0
  29. data/lib/minitest/ax_elements.rb +180 -0
  30. data/lib/mouse.rb +227 -0
  31. data/lib/rspec/expectations/ax_elements.rb +234 -0
  32. data/rakelib/gem.rake +3 -12
  33. data/rakelib/test.rake +15 -0
  34. data/test/helper.rb +20 -10
  35. data/test/integration/accessibility/test_core.rb +18 -0
  36. data/test/integration/accessibility/test_dsl.rb +40 -38
  37. data/test/integration/accessibility/test_enumerators.rb +1 -0
  38. data/test/integration/accessibility/test_graph.rb +0 -1
  39. data/test/integration/accessibility/test_qualifier.rb +2 -2
  40. data/test/integration/ax/test_application.rb +2 -9
  41. data/test/integration/ax/test_element.rb +0 -40
  42. data/test/integration/minitest/test_ax_elements.rb +89 -0
  43. data/test/integration/rspec/expectations/test_ax_elements.rb +102 -0
  44. data/test/sanity/accessibility/test_factory.rb +2 -2
  45. data/test/sanity/accessibility/test_highlighter.rb +56 -0
  46. data/test/sanity/accessibility/{test_pretty_printer.rb → test_pp_inspector.rb} +9 -9
  47. data/test/sanity/accessibility/test_statistics.rb +57 -0
  48. data/test/sanity/ax/test_application.rb +1 -16
  49. data/test/sanity/ax/test_element.rb +2 -2
  50. data/test/sanity/ax_elements/test_nsobject_inspect.rb +2 -4
  51. data/test/sanity/minitest/test_ax_elements.rb +17 -0
  52. data/test/sanity/rspec/expectations/test_ax_elements.rb +15 -0
  53. data/test/sanity/test_mouse.rb +22 -0
  54. data/test/test_core.rb +454 -0
  55. metadata +44 -69
  56. data/History.markdown +0 -41
  57. data/lib/accessibility/system_info.rb +0 -230
  58. data/lib/ax_elements/active_support_selections.rb +0 -10
  59. data/lib/ax_elements/mri.rb +0 -57
  60. data/test/sanity/accessibility/test_version.rb +0 -15
data/.yardopts CHANGED
@@ -8,7 +8,3 @@
8
8
  --hide-void-return
9
9
  lib/**/*.rb
10
10
  ext/**/*{.m,.c}
11
- -
12
- History.markdown
13
- CONTRIBUTING.markdown
14
-
data/README.markdown CHANGED
@@ -64,12 +64,13 @@ The code from the demo video is right here:
64
64
 
65
65
  ## Getting Setup
66
66
 
67
- You will need Ruby 1.9.3 or a MacRuby nightly build for
68
- installation. You can get help installing Ruby 1.9.3 from the
69
- [Ruby Website](http://www.ruby-lang.org), or help installing MacRuby
70
- from the
71
- [Setup MacRuby](https://github.com/MacRuby/MacRuby/wiki/Setting-up-MacRuby)
72
- guide on Github.
67
+ You need to have the OS X command line tools installed in order to
68
+ build and install AXElements, but you will also need Xcode if you want
69
+ to run the test suite (sorry). Go ahead and install the tools now if you
70
+ haven't done that yet, I'll wait. Once you have the developer tools,
71
+ you should install MacRuby, version 0.12 or newer is required. If you
72
+ are on Snow Leopard, you will also need to install the
73
+ [Bridge Support Preview](http://www.macruby.org/blog/2010/10/08/bridgesupport-preview.html).
73
74
 
74
75
  You will also need to make sure you "enable access for assistive devices".
75
76
  This can be done in System Preferences in the Universal Access section:
@@ -97,11 +98,11 @@ Once all the setup is finished, you can start up AXElements in IRB:
97
98
  irb -rubygems -rax_elements
98
99
  ```
99
100
 
100
- __NOTE__: If you are not using RVM, but are using MacRuby, then you
101
- should use `macrake` instead of `rake`, and `macirb` instead of `irb`,
102
- etc.. You may also need to add `sudo` to your command when you install
103
- the gem. If you are not using RVM with MacRuby, but have RVM
104
- installed, remember to disable it like so:
101
+ __NOTE__: If you are not using RVM, then you should use `macrake`
102
+ instead of `rake`, and `macirb` instead of `irb`, etc.. You may also
103
+ need to add `sudo` to your command when you install the gem. If you
104
+ are not using RVM with MacRuby, but have RVM installed, remember to
105
+ disable it like so:
105
106
 
106
107
  ```bash
107
108
  rvm use system
@@ -123,11 +124,9 @@ as some of the technical the technical underpinnings of AXElements.
123
124
 
124
125
  ## Development
125
126
 
126
- [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/Marketcircle/AXElements)
127
-
128
127
  AXElements has reached a point where the main focus is stability,
129
- documentation, and additional conveniences. It will be out of this
130
- world, so we're code naming the next version "Lunatone".
128
+ features, and documentation. It will be out of this world, so we're
129
+ code naming the next version "Lunatone".
131
130
 
132
131
  ![The Moon](https://github.com/Marketcircle/AXElements/raw/gh-pages/images/next_version.png)
133
132
 
@@ -179,12 +178,18 @@ disabled by default. In order to enable them you need to set the
179
178
 
180
179
  ## Contributing to AXElements
181
180
 
182
- See {file:CONTRIBUTING.markdown}
181
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
182
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
183
+ * Fork the project
184
+ * Start a feature/bugfix branch
185
+ * Commit and push until you are happy with your contribution
186
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
187
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
183
188
 
184
189
 
185
190
  ## Copyright
186
191
 
187
- Copyright (c) 2010-2013, Marketcircle Inc.
192
+ Copyright (c) 2010-2012, Marketcircle Inc.
188
193
  All rights reserved.
189
194
 
190
195
  Redistribution and use in source and binary forms, with or without
data/Rakefile CHANGED
@@ -11,4 +11,4 @@ desc 'Compile C extensions'
11
11
  task :ext => 'ext:key_coder'
12
12
 
13
13
  desc 'Run all tests'
14
- task :test => ['test:sanity', 'test:integration']
14
+ task :test => ['test:core', 'test:sanity', 'test:integration', 'test:cruby']
@@ -10,7 +10,7 @@ else
10
10
  clang = `which clang`.chomp
11
11
  if clang.empty?
12
12
  $stdout.puts "Clang not installed. Cannot build C extension"
13
- raise "Clang not installed. Cannot build C extension"
13
+ exit 1
14
14
  else
15
15
  RbConfig::MAKEFILE_CONFIG["CC"] = clang
16
16
  RbConfig::MAKEFILE_CONFIG["CXX"] = clang
@@ -26,8 +26,7 @@
26
26
  * @return [Hash{String=>Number}]
27
27
  */
28
28
 
29
- static
30
- VALUE
29
+ static VALUE
31
30
  keycoder_dynamic_mapping()
32
31
  {
33
32
 
@@ -92,8 +91,7 @@ keycoder_dynamic_mapping()
92
91
  * @return [true]
93
92
  */
94
93
 
95
- static
96
- VALUE
94
+ static VALUE
97
95
  keycoder_post_event(VALUE self, VALUE event)
98
96
  {
99
97
  VALUE code = rb_ary_entry(event, 0);
data/lib/accessibility.rb CHANGED
@@ -46,7 +46,7 @@ class << Accessibility
46
46
  # @param bundle [String] a bundle identifier
47
47
  # @return [AX::Application,nil]
48
48
  def application_with_bundle_identifier bundle
49
- $stderr.puts "#{__method__} is DEPRECATED: Use AX::Application.new instead"
49
+ $stderr.puts 'DEPRECATED: Use AX::Application.new instead'
50
50
  if app_running?(bundle) || launch_application(bundle)
51
51
  10.times do
52
52
  if app_running?(bundle) && (app = try_wrapping(bundle))
@@ -75,7 +75,7 @@ class << Accessibility
75
75
  # @param [String] name name of the application to launch
76
76
  # @return [AX::Application,nil]
77
77
  def application_with_name name
78
- $stderr.puts "#{__method__} is DEPRECATED: Use AX::Application.new instead"
78
+ $stderr.puts 'DEPRECATED: Use AX::Application.new instead'
79
79
  AX::Application.new name
80
80
  end
81
81
 
@@ -102,7 +102,7 @@ class << Accessibility
102
102
  # @return [AX::Application]
103
103
  def try_wrapping bundle
104
104
  AX::Application.new bundle
105
- rescue RuntimeError
105
+ rescue RuntimeError => e
106
106
  nil
107
107
  end
108
108
 
@@ -0,0 +1,948 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ framework 'Cocoa'
4
+ require 'ax_elements/core_graphics_workaround'
5
+
6
+ # check that the Accessibility APIs are enabled and are available to MacRuby
7
+ begin
8
+ unless AXAPIEnabled()
9
+ raise RuntimeError, <<-EOS
10
+ ------------------------------------------------------------------------
11
+ Universal Access is disabled on this machine.
12
+
13
+ Please enable it in the System Preferences.
14
+ ------------------------------------------------------------------------
15
+ EOS
16
+ end
17
+ rescue NoMethodError
18
+ raise NotImplementedError, <<-EOS
19
+ ------------------------------------------------------------------------
20
+ You need to install the latest BridgeSupport preview so that AXElements
21
+ has access to CoreFoundation.
22
+ ------------------------------------------------------------------------
23
+ EOS
24
+ end
25
+
26
+
27
+ require 'accessibility/version'
28
+ require 'accessibility/statistics'
29
+
30
+
31
+ ##
32
+ # Core abstraction layer that that interacts with OS X Accessibility
33
+ # APIs (AXAPI). This provides a generic object oriented mixin for
34
+ # the low level APIs. In MacRuby, bridge support turns C structs into
35
+ # "first class" objects. To that end, instead of adding an extra allocation
36
+ # to wrap the object, we will simply add a mixin to add some basic
37
+ # functionality. A more Ruby-ish wrapper is available through {AX::Element}.
38
+ # The complication in making the mixin more fully featured is that the class
39
+ # which we mix into is abstract and shared for a number of different C structs.
40
+ #
41
+ # This module is responsible for handling pointers and dealing with error
42
+ # codes for functions that make use of them. The methods in this class
43
+ # provide a cleaner, more Ruby-ish interface to the low level CoreFoundation
44
+ # functions that compose AXAPI than are natively available.
45
+ #
46
+ # @example
47
+ #
48
+ # element = AXUIElementCreateSystemWide()
49
+ # element.attributes # => ["AXRole", "AXChildren", ...]
50
+ # element.size_of "AXChildren" # => 12
51
+ #
52
+ module Accessibility::Element
53
+
54
+
55
+ # @!group Attributes
56
+
57
+ ##
58
+ # @todo Invalid elements do not always raise an error.
59
+ # This is a bug that should be logged with Apple.
60
+ #
61
+ # Get the list of attributes for the element. As a convention, this
62
+ # method will return an empty array if the backing element is no longer
63
+ # alive.
64
+ #
65
+ # @example
66
+ #
67
+ # button.attributes # => ["AXRole", "AXRoleDescription", ...]
68
+ #
69
+ # @return [Array<String>]
70
+ def attributes
71
+ @attributes ||= (
72
+ STATS.increment :AttributeNames
73
+
74
+ ptr = Pointer.new ARRAY
75
+ code = AXUIElementCopyAttributeNames(self, ptr)
76
+
77
+ case code
78
+ when 0 then ptr.value
79
+ when KAXErrorInvalidUIElement then []
80
+ else handle_error code
81
+ end
82
+ )
83
+ end
84
+
85
+ ##
86
+ # Fetch the value for an attribute. CoreFoundation wrapped objects
87
+ # will be unwrapped for you, if you expect to get a {CFRange} you
88
+ # will be given a {Range} instead.
89
+ #
90
+ # As a convention, if the backing element is no longer alive then
91
+ # any attribute value will return `nil`, except for `KAXChildrenAttribute`
92
+ # which will return an empty array. This is a debatably necessary evil,
93
+ # inquire for details.
94
+ #
95
+ # If the attribute is not supported by the element then a exception
96
+ # will be raised.
97
+ #
98
+ # @example
99
+ # window.attribute KAXTitleAttribute # => "HotCocoa Demo"
100
+ # window.attribute KAXSizeAttribute # => #<CGSize width=10.0 height=88>
101
+ # window.attribute KAXParentAttribute # => #<AXUIElementRef>
102
+ # window.attribute KAXNoValueAttribute # => nil
103
+ #
104
+ # @param name [String]
105
+ def attribute name
106
+ STATS.increment :AttributeValue
107
+
108
+ ptr = Pointer.new :id
109
+ code = AXUIElementCopyAttributeValue(self, name, ptr)
110
+
111
+ case code
112
+ when 0
113
+ ptr.value.to_ruby
114
+ when KAXErrorNoValue, KAXErrorInvalidUIElement
115
+ name == KAXChildrenAttribute ? [] : nil
116
+ else
117
+ handle_error code, name
118
+ end
119
+ end
120
+
121
+ ##
122
+ # Shortcut for getting the `KAXRoleAttribute`. Remember that
123
+ # dead elements may return `nil` for their role.
124
+ #
125
+ # @example
126
+ #
127
+ # window.role # => KAXWindowRole
128
+ #
129
+ # @return [String,nil]
130
+ def role
131
+ STATS.increment :Role
132
+ attribute KAXRoleAttribute
133
+ end
134
+
135
+ ##
136
+ # @note You might get `nil` back as the subrole as AXWebArea
137
+ # objects are known to do this. You need to check. :(
138
+ #
139
+ # Shortcut for getting the `KAXSubroleAttribute`.
140
+ #
141
+ # @example
142
+ # window.subrole # => "AXDialog"
143
+ # web_area.subrole # => nil
144
+ #
145
+ # @return [String,nil]
146
+ def subrole
147
+ STATS.increment :Subrole
148
+ attribute KAXSubroleAttribute
149
+ end
150
+
151
+ ##
152
+ # Shortcut for getting the `KAXChildrenAttribute`. An exception will
153
+ # be raised if the object does not have children.
154
+ #
155
+ # @example
156
+ #
157
+ # app.children # => [MenuBar, Window, ...]
158
+ #
159
+ # @return [Array<AXUIElementRef>]
160
+ def children
161
+ STATS.increment :Children
162
+ attribute KAXChildrenAttribute
163
+ end
164
+
165
+ ##
166
+ # Shortcut for getting the `KAXValueAttribute`.
167
+ #
168
+ # @example
169
+ #
170
+ # label.value # => "Mark Rada"
171
+ # slider.value # => 42
172
+ #
173
+ def value
174
+ STATS.increment :Value
175
+ attribute KAXValueAttribute
176
+ end
177
+
178
+ ##
179
+ # Get the process identifier (PID) of the application that the element
180
+ # belongs to.
181
+ #
182
+ # This method will return `0` if the element is dead or if the receiver
183
+ # is the the system wide element.
184
+ #
185
+ # @example
186
+ #
187
+ # window.pid # => 12345
188
+ # Element.system_wide.pid # => 0
189
+ #
190
+ # @return [Fixnum]
191
+ def pid
192
+ @pid ||= (
193
+ STATS.increment :PID
194
+
195
+ ptr = Pointer.new :int
196
+ code = AXUIElementGetPid(self, ptr)
197
+
198
+ case code
199
+ when 0
200
+ ptr.value
201
+ when KAXErrorInvalidUIElement
202
+ self == Accessibility::Element.system_wide ? 0 : handle_error(code)
203
+ else
204
+ handle_error code
205
+ end
206
+ )
207
+ end
208
+
209
+ ##
210
+ # Return whether or not the receiver is "dead".
211
+ #
212
+ # A dead element is one that is no longer in the app's view
213
+ # hierarchy. This is not the same as visibility; an element that is
214
+ # invalid will not be visible, but an invisible element might still
215
+ # be valid.
216
+ def invalid?
217
+ STATS.increment :Invalid?
218
+ AXUIElementCopyAttributeValue(self, KAXRoleAttribute, Pointer.new(:id)) ==
219
+ KAXErrorInvalidUIElement
220
+ end
221
+
222
+ ##
223
+ # @note It has been observed that some elements may lie with this value.
224
+ # Bugs should be reported to the app developers in those cases.
225
+ #
226
+ # Get the size of the array for attributes that would return an array.
227
+ # When performance matters, this is much faster than getting the array
228
+ # and asking for the size.
229
+ #
230
+ # If there is a failure or the backing element is no longer alive, this
231
+ # method will return `0`.
232
+ #
233
+ # @example
234
+ #
235
+ # window.size_of KAXChildrenAttribute # => 19
236
+ # table.size_of KAXRowsAttribute # => 100
237
+ #
238
+ # @param name [String]
239
+ # @return [Number]
240
+ def size_of name
241
+ STATS.increment :AttributeSizeOf
242
+
243
+ ptr = Pointer.new :long_long
244
+ code = AXUIElementGetAttributeValueCount(self, name, ptr)
245
+
246
+ case code
247
+ when 0
248
+ ptr.value
249
+ when KAXErrorFailure, KAXErrorNoValue, KAXErrorInvalidUIElement
250
+ 0
251
+ else
252
+ handle_error code, name
253
+ end
254
+ end
255
+
256
+ ##
257
+ # Returns whether or not an attribute is writable. Often, you will
258
+ # want/need to check writability of an attribute before trying call
259
+ # {#set} for the attribute.
260
+ #
261
+ # In case of internal error or if the element dies, this method will
262
+ # return `false`.
263
+ #
264
+ # @example
265
+ #
266
+ # window.writable? KAXSizeAttribute # => true
267
+ # window.writable? KAXTitleAttribute # => false
268
+ #
269
+ # @param name [String]
270
+ def writable? name
271
+ STATS.increment :Writable?
272
+
273
+ ptr = Pointer.new :bool
274
+ code = AXUIElementIsAttributeSettable(self, name, ptr)
275
+
276
+ case code
277
+ when 0
278
+ ptr.value
279
+ when KAXErrorFailure, KAXErrorNoValue, KAXErrorInvalidUIElement
280
+ false
281
+ else
282
+ handle_error code, name
283
+ end
284
+ end
285
+
286
+ ##
287
+ # Set the given value for the given attribute. You do not need to
288
+ # worry about wrapping objects first, `Range` objects will also
289
+ # be automatically converted into `CFRange` objects (unless they
290
+ # have a negative index) and then wrapped.
291
+ #
292
+ # This method does not check writability of the attribute you are
293
+ # setting. If you need to check, use {#writable?} first to check.
294
+ #
295
+ # Unlike when reading attributes, writing to a dead element, and
296
+ # other error conditions, will raise an exception.
297
+ #
298
+ # @example
299
+ #
300
+ # set KAXValueAttribute, "hi" # => "hi"
301
+ # set KAXSizeAttribute, [250,250] # => [250,250]
302
+ # set KAXVisibleRangeAttribute, 0..3 # => 0..3
303
+ # set KAXVisibleRangeAttribute, 1...4 # => 1..3
304
+ #
305
+ # @param name [String]
306
+ def set name, value
307
+ STATS.increment :AttributeSet
308
+ code = AXUIElementSetAttributeValue(self, name, value.to_ax)
309
+ if code.zero?
310
+ value
311
+ else
312
+ handle_error code, name, value
313
+ end
314
+ end
315
+
316
+
317
+ # @!group Parameterized Attributes
318
+
319
+ ##
320
+ # Get the list of parameterized attributes for the element. If the
321
+ # element does not have parameterized attributes, then an empty
322
+ # list will be returned.
323
+ #
324
+ # Most elements do not have parameterized attributes, but the ones
325
+ # that do, have many.
326
+ #
327
+ # Similar to {#attributes}, this method will also return an empty
328
+ # array if the element is dead.
329
+ #
330
+ # @example
331
+ #
332
+ # text_area.parameterized_attributes # => ["AXStringForRange", ...]
333
+ # app.parameterized_attributes # => []
334
+ #
335
+ # @return [Array<String>]
336
+ def parameterized_attributes
337
+ @parameterized_attributes ||= (
338
+ STATS.increment :ParameterizedAttributes
339
+
340
+ ptr = Pointer.new ARRAY
341
+ code = AXUIElementCopyParameterizedAttributeNames(self, ptr)
342
+
343
+ case code
344
+ when 0 then ptr.value
345
+ when KAXErrorNoValue, KAXErrorInvalidUIElement then []
346
+ else handle_error code
347
+ end
348
+ )
349
+ end
350
+
351
+ ##
352
+ # Fetch the given pramaeterized attribute value for the given parameter.
353
+ # Low level objects, such as `AXUIElementRef` and {Boxed} objects, will
354
+ # be unwrapped for you automatically and {CFRange} objects will be turned
355
+ # into {Range} objects. Similarly, you do not need to worry about wrapping
356
+ # the parameter as that will be done for you (except for {Range} objects
357
+ # that use a negative index).
358
+ #
359
+ # As a convention, if the backing element is no longer alive, or the
360
+ # attribute does not exist, or a system failure occurs then you will
361
+ # receive `nil`.
362
+ #
363
+ # @example
364
+ #
365
+ # parameterized_attribute KAXStringForRangeParameterizedAttribute, 1..10
366
+ # # => "ello, worl"
367
+ #
368
+ # @param name [String]
369
+ def parameterized_attribute name, param
370
+ STATS.increment :ParameterizedAttribute
371
+
372
+ ptr = Pointer.new :id
373
+ code = AXUIElementCopyParameterizedAttributeValue(self, name, param.to_ax, ptr)
374
+
375
+ case code
376
+ when 0
377
+ ptr.value.to_ruby
378
+ when KAXErrorFailure, KAXErrorNoValue, KAXErrorInvalidUIElement
379
+ nil
380
+ else
381
+ handle_error code, name, param
382
+ end
383
+ end
384
+
385
+
386
+ # @!group Actions
387
+
388
+ ##
389
+ # Get the list of actions that the element can perform. If an element
390
+ # does not have actions, then an empty list will be returned. Dead
391
+ # elements will also return an empty array.
392
+ #
393
+ # @example
394
+ #
395
+ # button.actions # => ["AXPress"]
396
+ #
397
+ # @return [Array<String>]
398
+ def actions
399
+ @actions ||= (
400
+ STATS.increment :Actions
401
+
402
+ ptr = Pointer.new ARRAY
403
+ code = AXUIElementCopyActionNames(self, ptr)
404
+
405
+ case code
406
+ when 0 then ptr.value
407
+ when KAXErrorInvalidUIElement then []
408
+ else handle_error code
409
+ end
410
+ )
411
+ end
412
+
413
+ ##
414
+ # Ask an element to perform the given action. This method will always
415
+ # return true or raise an exception. Actions should never fail, but
416
+ # there are some extreme edge cases (e.g. out of memory, etc.).
417
+ #
418
+ # Unlike when reading attributes, performing an action on a dead element
419
+ # will raise an exception.
420
+ #
421
+ # @example
422
+ #
423
+ # perform KAXPressAction # => true
424
+ #
425
+ # @param action [String]
426
+ # @return [Boolean]
427
+ def perform action
428
+ STATS.increment :Perform
429
+ code = AXUIElementPerformAction(self, action)
430
+ if code.zero?
431
+ true
432
+ else
433
+ handle_error code, action
434
+ end
435
+ end
436
+
437
+ ##
438
+ # Post the list of given keyboard events to the element. This only
439
+ # applies if the given element is an application object or the
440
+ # system wide object. The focused element will receive the events.
441
+ #
442
+ # Events could be generated from a string using output from
443
+ # {Accessibility::String#keyboard_events_for}.
444
+ #
445
+ # Events are number/boolean tuples, where the number is a keycode
446
+ # and the boolean is the keypress state (true is keydown, false is
447
+ # keyup).
448
+ #
449
+ # You can learn more about keyboard events from the
450
+ # [Keyboard Events documentation](http://github.com/Marketcircle/AXElements/wiki/Keyboarding).
451
+ #
452
+ # @example
453
+ #
454
+ # include Accessibility::String
455
+ # events = keyboard_events_for "Hello, world!\n"
456
+ # app.post events
457
+ #
458
+ # @param events [Array<Array(Number,Boolean)>]
459
+ def post events
460
+ events.each do |event|
461
+ STATS.increment :KeyboardEvent
462
+ code = AXUIElementPostKeyboardEvent(self, 0, *event)
463
+ handle_error code unless code.zero?
464
+ sleep @@key_rate
465
+ end
466
+ sleep 0.1 # in many cases, UI is not done updating right away
467
+ end
468
+
469
+ ##
470
+ # The delay between key events. The default value is `0.01`, which
471
+ # should be about 50 characters per second (down and up are separate
472
+ # events).
473
+ #
474
+ # This is just a magic number from trial and error. Both the repeat
475
+ # interval (NXKeyRepeatInterval) and threshold (NXKeyRepeatThreshold)
476
+ # were tried, but were way too big.
477
+ #
478
+ # @return [Number]
479
+ def self.key_rate
480
+ @@key_rate
481
+ end
482
+ @@key_rate = 0.009
483
+
484
+ ##
485
+ # Set the delay between key events. This value is used by {#post}
486
+ # to slow down the typing speed so apps do not get overloaded.
487
+ #
488
+ # You can pass either a precise value for sleeping (a `Float` or
489
+ # `Fixnum`), or you can use a preset symbol:
490
+ #
491
+ # - `:very_slow`
492
+ # - `:slow`
493
+ # - `:normal`/`:default`
494
+ # - `:fast`
495
+ # - `:zomg`
496
+ #
497
+ # The `:zomg` setting will be too fast in almost all cases, but
498
+ # it is fun to watch.
499
+ #
500
+ # @param [Number,Symbol]
501
+ def self.key_rate= value
502
+ @@key_rate = case value
503
+ when :very_slow then 0.9
504
+ when :slow then 0.09
505
+ when :normal, :default then 0.009
506
+ when :fast then 0.0009
507
+ when :zomg then 0.00009
508
+ else value
509
+ end
510
+ end
511
+
512
+
513
+ # @!group Element Hierarchy Entry Points
514
+
515
+ ##
516
+ # Find the top most element at a point on the screen that belongs to the
517
+ # backing application. If the backing element is the system wide object
518
+ # then the return is the top most element regardless of application.
519
+ #
520
+ # The coordinates should be specified using the flipped coordinate
521
+ # system (origin is in the top-left, increasing downward and to the right
522
+ # as if reading a book in English).
523
+ #
524
+ # If more than one element is at the position then the z-order of the
525
+ # elements will be used to determine which is "on top".
526
+ #
527
+ # This method will safely return `nil` if there is no UI element at the
528
+ # give point.
529
+ #
530
+ # @example
531
+ #
532
+ # Element.system_wide.element_at [453, 200] # table
533
+ # app.element_at CGPoint.new(453, 200) # table
534
+ #
535
+ # @param point [#to_point]
536
+ # @return [AXUIElementRef,nil]
537
+ def element_at point
538
+ STATS.increment :ElementAtPosition
539
+
540
+ ptr = Pointer.new ELEMENT
541
+ code = AXUIElementCopyElementAtPosition(self, *point.to_point, ptr)
542
+
543
+ case code
544
+ when 0
545
+ ptr.value.to_ruby
546
+ when KAXErrorNoValue
547
+ nil
548
+ when KAXErrorInvalidUIElement
549
+ unless self == Accessibility::Element.system_wide
550
+ Accessibility::Element.system_wide.element_at point
551
+ end
552
+ else
553
+ handle_error code, point, nil, nil
554
+ end
555
+ end
556
+
557
+ ##
558
+ # Get the application object object for an application given the
559
+ # process identifier (PID) for that application.
560
+ #
561
+ # @example
562
+ #
563
+ # app = Element.application_for 54743 # => #<AXUIElementRef>
564
+ #
565
+ # @param pid [Number]
566
+ # @return [AXUIElementRef]
567
+ def self.application_for pid
568
+ NSRunLoop.currentRunLoop.runUntilDate Time.now
569
+ if NSRunningApplication.runningApplicationWithProcessIdentifier pid
570
+ STATS.increment :CreateApplication
571
+ AXUIElementCreateApplication(pid)
572
+ else
573
+ raise ArgumentError, 'pid must belong to a running application'
574
+ end
575
+ end
576
+
577
+
578
+ # @!group Misc.
579
+
580
+ ##
581
+ # Create a new reference to the system wide object. This is very useful when
582
+ # working with the system wide object as caching the system wide reference
583
+ # does not seem to work often.
584
+ #
585
+ # @example
586
+ #
587
+ # system_wide # => #<AXUIElementRefx00000000>
588
+ #
589
+ # @return [AXUIElementRef]
590
+ def self.system_wide
591
+ STATS.increment :SystemWide
592
+ AXUIElementCreateSystemWide()
593
+ end
594
+
595
+ ##
596
+ # Returns the application reference for the application that the receiver
597
+ # belongs to.
598
+ #
599
+ # @return [AXUIElementRef]
600
+ def application
601
+ Accessibility::Element.application_for pid
602
+ end
603
+
604
+ ##
605
+ # Unwrap an `AXValue` into the `Boxed` instance that it is supposed
606
+ # to be. This will only work for the most common boxed types, you will
607
+ # need to check the AXAPI documentation for an up to date list.
608
+ #
609
+ # @example
610
+ #
611
+ # wrapped_point.to_ruby # => #<CGPoint x=44.3 y=99.0>
612
+ # wrapped_range.to_ruby # => #<CFRange begin=7 length=100>
613
+ # wrapped_thing.to_ruby # => wrapped_thing
614
+ #
615
+ # @return [Boxed]
616
+ def to_ruby
617
+ type = AXValueGetType(self)
618
+ return self if type.zero?
619
+
620
+ STATS.increment :Unwrap
621
+ ptr = Pointer.new BOX_TYPES[type]
622
+ AXValueGetValue(self, type, ptr)
623
+ ptr.value.to_ruby
624
+ end
625
+
626
+
627
+ # @!group Debug
628
+
629
+ ##
630
+ # Change the timeout value for the element. If you change the timeout
631
+ # on the system wide object, it affets all timeouts.
632
+ #
633
+ # Setting the global timeout to `0` seconds will reset the timeout value
634
+ # to the system default. The system default timeout value is `6 seconds`
635
+ # as of the writing of this documentation, but Apple has not publicly
636
+ # documented this (we had to ask in person at WWDC).
637
+ #
638
+ # @param seconds [Number]
639
+ # @return [Number]
640
+ def set_timeout_to seconds
641
+ STATS.increment :SetTimeout
642
+ case code = AXUIElementSetMessagingTimeout(self, seconds)
643
+ when 0 then seconds
644
+ else handle_error code, seconds
645
+ end
646
+ end
647
+
648
+
649
+ private
650
+
651
+ # @!group Error Handling
652
+
653
+ ##
654
+ # @private
655
+ #
656
+ # Mapping of `AXError` values to static information on how to handle
657
+ # the error. Used by {handle_error}.
658
+ #
659
+ # @return [Hash{Number=>Array(Symbol,Range)}]
660
+ AXERROR = {
661
+ KAXErrorFailure => [
662
+ RuntimeError,
663
+ lambda { |*args|
664
+ "A system failure occurred with #{args[0].inspect}, stopping to be safe"
665
+ }
666
+ ],
667
+ KAXErrorIllegalArgument => [
668
+ ArgumentError,
669
+ lambda { |*args|
670
+ case args.size
671
+ when 1
672
+ "#{args[0].inspect} is not an AXUIElementRef"
673
+ when 2
674
+ "Either the element #{args[0].inspect} or the attribute/action" +
675
+ "#{args[1].inspect} is not a legal argument"
676
+ when 3
677
+ "You can't get/set #{args[1].inspect} with/to #{args[2].inspect} " +
678
+ "for #{args[0].inspect}"
679
+ when 4
680
+ "The point #{args[1].to_point.inspect} is not a valid point, " +
681
+ "or #{args[0].inspect} is not an AXUIElementRef"
682
+ end
683
+ }
684
+ ],
685
+ KAXErrorInvalidUIElement => [
686
+ ArgumentError,
687
+ lambda { |*args|
688
+ "#{args[0].inspect} is no longer a valid reference"
689
+ }
690
+ ],
691
+ KAXErrorInvalidUIElementObserver => [
692
+ ArgumentError,
693
+ lambda { |*args|
694
+ 'AXElements no longer supports notifications'
695
+ }
696
+ ],
697
+ KAXErrorCannotComplete => [
698
+ RuntimeError,
699
+ lambda { |*args|
700
+ NSRunLoop.currentRunLoop.runUntilDate Time.now # spin the run loop once
701
+ pid = args[0].pid
702
+ app = NSRunningApplication.runningApplicationWithProcessIdentifier pid
703
+ if app
704
+ "An unspecified error occurred using #{args[0].inspect} with AXAPI, maybe a timeout :("
705
+ else
706
+ "Application for pid=#{pid} is no longer running. Maybe it crashed?"
707
+ end
708
+ }
709
+ ],
710
+ KAXErrorAttributeUnsupported => [
711
+ ArgumentError,
712
+ lambda { |*args|
713
+ "#{args[0].inspect} does not have a #{args[1].inspect} attribute"
714
+ }
715
+ ],
716
+ KAXErrorActionUnsupported => [
717
+ ArgumentError,
718
+ lambda { |*args|
719
+ "#{args[0].inspect} does not have a #{args[1].inspect} action"
720
+ }
721
+ ],
722
+ KAXErrorNotificationUnsupported => [
723
+ ArgumentError,
724
+ lambda { |*args|
725
+ 'AXElements no longer supports notifications'
726
+ }
727
+ ],
728
+ KAXErrorNotImplemented => [
729
+ NotImplementedError,
730
+ lambda { |*args|
731
+ "The program that owns #{args[0].inspect} does not work with AXAPI properly"
732
+ }
733
+ ],
734
+ KAXErrorNotificationAlreadyRegistered => [
735
+ ArgumentError,
736
+ lambda { |*args|
737
+ 'AXElements no longer supports notifications'
738
+ }
739
+ ],
740
+ KAXErrorNotificationNotRegistered => [
741
+ RuntimeError,
742
+ lambda { |*args|
743
+ 'AXElements no longer supports notifications'
744
+ }
745
+ ],
746
+ KAXErrorAPIDisabled => [
747
+ RuntimeError,
748
+ lambda { |*args|
749
+ 'AXAPI has been disabled'
750
+ }
751
+ ],
752
+ KAXErrorNoValue => [
753
+ RuntimeError,
754
+ lambda { |*args|
755
+ 'AXElements internal error. ENoValue should be handled internally!'
756
+ }
757
+ ],
758
+ KAXErrorParameterizedAttributeUnsupported => [
759
+ ArgumentError,
760
+ lambda { |*args|
761
+ "#{args[0].inspect} does not have a #{args[1].inspect} parameterized attribute"
762
+ }
763
+ ],
764
+ KAXErrorNotEnoughPrecision => [
765
+ RuntimeError,
766
+ lambda { |*args|
767
+ 'AXAPI said there was not enough precision ¯\(°_o)/¯'
768
+ }
769
+ ]
770
+ }
771
+
772
+ # @param code [Number]
773
+ def handle_error code, *args
774
+ raise RuntimeError, 'assertion failed: code 0 means success!' if code.zero?
775
+ klass, handler = AXERROR.fetch code, [
776
+ RuntimeError,
777
+ lambda { |*args| "An unknown error code was returned [#{code}]:#{inspect}" }
778
+ ]
779
+ raise klass, handler.call(self, *args), caller(1)
780
+ end
781
+
782
+
783
+ # @!endgroup
784
+
785
+
786
+ ##
787
+ # @private
788
+ #
789
+ # `Pointer` type encoding for `CFArrayRef` objects.
790
+ #
791
+ # @return [String]
792
+ ARRAY = '^{__CFArray}'
793
+
794
+ ##
795
+ # @private
796
+ #
797
+ # `Pointer` type encoding for `AXUIElementRef` objects.
798
+ #
799
+ # @return [String]
800
+ ELEMENT = '^{__AXUIElement}'
801
+
802
+ ##
803
+ # Map of type encodings used for wrapping structs when coming from
804
+ # an `AXValueRef`.
805
+ #
806
+ # The list is order sensitive, which is why we unshift nil, but
807
+ # should probably be more rigorously defined at runtime.
808
+ #
809
+ # @return [String,nil]
810
+ BOX_TYPES = [CGPoint, CGSize, CGRect, CFRange].map!(&:type).unshift(nil)
811
+
812
+ end
813
+
814
+ # hack to find the __NSCFType class and mix things in
815
+ klass = AXUIElementCreateSystemWide().class
816
+ klass.send :include, Accessibility::Element
817
+
818
+ ##
819
+ # AXElements extensions to the `Boxed` class. The `Boxed` class is
820
+ # simply an abstract base class for structs that MacRuby can use
821
+ # via bridge support.
822
+ class Boxed
823
+ ##
824
+ # Returns the number that AXAPI uses in order to know how to wrap
825
+ # a struct.
826
+ #
827
+ # @return [Number]
828
+ def self.ax_value
829
+ raise NotImplementedError, "#{inspect}:#{self.class} cannot be wraped"
830
+ end
831
+
832
+ ##
833
+ # Create an `AXValueRef` from the `Boxed` instance. This will only
834
+ # work if for the most common boxed types, you will need to check
835
+ # the AXAPI documentation for an up to date list.
836
+ #
837
+ # @example
838
+ #
839
+ # CGPoint.new(12, 34).to_ax # => #<AXValueRef:0x455678e2>
840
+ # CGSize.new(56, 78).to_ax # => #<AXValueRef:0x555678e2>
841
+ #
842
+ # @return [AXValueRef]
843
+ def to_ax
844
+ STATS.increment :Wrap
845
+ klass = self.class
846
+ ptr = Pointer.new klass.type
847
+ ptr.assign self
848
+ AXValueCreate(klass.ax_value, ptr)
849
+ end
850
+ end
851
+
852
+ # AXElements extensions for `CFRange`.
853
+ class << CFRange
854
+ # (see Boxed.ax_value)
855
+ def ax_value; KAXValueCFRangeType end
856
+ end
857
+ # AXElements extensions for `CGSize`.
858
+ class << CGSize
859
+ # (see Boxed.ax_value)
860
+ def ax_value; KAXValueCGSizeType end
861
+ end
862
+ # AXElements extensions for `CGRect`.
863
+ class << CGRect
864
+ # (see Boxed.ax_value)
865
+ def ax_value; KAXValueCGRectType end
866
+ end
867
+ # AXElements extensions for `CGPoint`.
868
+ class << CGPoint
869
+ # (see Boxed.ax_value)
870
+ def ax_value; KAXValueCGPointType end
871
+ end
872
+
873
+
874
+ # AXElements extensions for `NSObject`.
875
+ class NSObject
876
+ ##
877
+ # Return an object safe for passing to AXAPI.
878
+ def to_ax; self end
879
+ ##
880
+ # Return a usable object from an AXAPI pointer.
881
+ def to_ruby; self end
882
+ end
883
+
884
+ # AXElements extensions for `Range`.
885
+ class Range
886
+ # @return [AXValueRef]
887
+ def to_ax
888
+ raise ArgumentError, "can't convert negative index" if last < 0 || first < 0
889
+ length = if exclude_end?
890
+ last - first
891
+ else
892
+ last - first + 1
893
+ end
894
+ CFRange.new(first, length).to_ax
895
+ end
896
+ end
897
+
898
+ # AXElements extensions for `CFRange`.
899
+ class CFRange
900
+ # @return [Range]
901
+ def to_ruby
902
+ Range.new location, (location + length - 1)
903
+ end
904
+ end
905
+
906
+
907
+ # AXElements extensions to `NSArray`.
908
+ class NSArray
909
+ # @return [CGPoint]
910
+ def to_point; CGPoint.new(first, at(1)) end
911
+ # @return [CGSize]
912
+ def to_size; CGSize.new(first, at(1)) end
913
+ # @return [CGRect]
914
+ def to_rect; CGRectMake(*self[0..3]) end
915
+ ##
916
+ # Override `super` to exploit trivial parallelism.
917
+ #
918
+ # @return [Array]
919
+ def to_ruby
920
+ @out = Array.new(self.size)
921
+ Dispatch::Queue.concurrent.apply(self.size) do |index|
922
+ @out[index] = self[index].to_ruby
923
+ end
924
+ @out
925
+ end
926
+ end
927
+
928
+ # AXElements extensions for `CGPoint`.
929
+ class CGPoint
930
+ # @return [CGPoint]
931
+ def to_point; self end
932
+ end
933
+
934
+ ##
935
+ # AXElements extensions for `NSURL`.
936
+ class NSString
937
+ ##
938
+ # Create an NSURL using the receiver as the initialization string.
939
+ # If the receiver is not a valid URL then `nil` will be returned.
940
+ #
941
+ # This exists because of
942
+ # [rdar://11207662](http://openradar.appspot.com/11207662).
943
+ #
944
+ # @return [NSURL,nil]
945
+ def to_url
946
+ NSURL.URLWithString self
947
+ end
948
+ end