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.
- data/.yardopts +0 -4
- data/README.markdown +22 -17
- data/Rakefile +1 -1
- data/ext/accessibility/key_coder/extconf.rb +1 -1
- data/ext/accessibility/key_coder/key_coder.c +2 -4
- data/lib/accessibility.rb +3 -3
- data/lib/accessibility/core.rb +948 -0
- data/lib/accessibility/dsl.rb +30 -186
- data/lib/accessibility/enumerators.rb +1 -0
- data/lib/accessibility/factory.rb +78 -134
- data/lib/accessibility/graph.rb +5 -9
- data/lib/accessibility/highlighter.rb +86 -0
- data/lib/accessibility/{pretty_printer.rb → pp_inspector.rb} +4 -3
- data/lib/accessibility/qualifier.rb +3 -5
- data/lib/accessibility/screen_recorder.rb +217 -0
- data/lib/accessibility/statistics.rb +57 -0
- data/lib/accessibility/translator.rb +23 -32
- data/lib/accessibility/version.rb +2 -22
- data/lib/ax/application.rb +20 -159
- data/lib/ax/element.rb +42 -32
- data/lib/ax/scroll_area.rb +5 -6
- data/lib/ax/systemwide.rb +1 -33
- data/lib/ax_elements.rb +1 -9
- data/lib/ax_elements/core_graphics_workaround.rb +5 -0
- data/lib/ax_elements/nsarray_compat.rb +17 -97
- data/lib/ax_elements/vendor/inflection_data.rb +66 -0
- data/lib/ax_elements/vendor/inflections.rb +176 -0
- data/lib/ax_elements/vendor/inflector.rb +306 -0
- data/lib/minitest/ax_elements.rb +180 -0
- data/lib/mouse.rb +227 -0
- data/lib/rspec/expectations/ax_elements.rb +234 -0
- data/rakelib/gem.rake +3 -12
- data/rakelib/test.rake +15 -0
- data/test/helper.rb +20 -10
- data/test/integration/accessibility/test_core.rb +18 -0
- data/test/integration/accessibility/test_dsl.rb +40 -38
- data/test/integration/accessibility/test_enumerators.rb +1 -0
- data/test/integration/accessibility/test_graph.rb +0 -1
- data/test/integration/accessibility/test_qualifier.rb +2 -2
- data/test/integration/ax/test_application.rb +2 -9
- data/test/integration/ax/test_element.rb +0 -40
- data/test/integration/minitest/test_ax_elements.rb +89 -0
- data/test/integration/rspec/expectations/test_ax_elements.rb +102 -0
- data/test/sanity/accessibility/test_factory.rb +2 -2
- data/test/sanity/accessibility/test_highlighter.rb +56 -0
- data/test/sanity/accessibility/{test_pretty_printer.rb → test_pp_inspector.rb} +9 -9
- data/test/sanity/accessibility/test_statistics.rb +57 -0
- data/test/sanity/ax/test_application.rb +1 -16
- data/test/sanity/ax/test_element.rb +2 -2
- data/test/sanity/ax_elements/test_nsobject_inspect.rb +2 -4
- data/test/sanity/minitest/test_ax_elements.rb +17 -0
- data/test/sanity/rspec/expectations/test_ax_elements.rb +15 -0
- data/test/sanity/test_mouse.rb +22 -0
- data/test/test_core.rb +454 -0
- metadata +44 -69
- data/History.markdown +0 -41
- data/lib/accessibility/system_info.rb +0 -230
- data/lib/ax_elements/active_support_selections.rb +0 -10
- data/lib/ax_elements/mri.rb +0 -57
- data/test/sanity/accessibility/test_version.rb +0 -15
data/.yardopts
CHANGED
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
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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,
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
130
|
-
|
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
|
-
|
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-
|
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
@@ -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
|
-
|
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
|
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
|
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
|