AXElements 0.7.8 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +1 -10
- data/README.markdown +7 -14
- data/ext/accessibility/key_coder/key_coder.c +7 -0
- data/lib/AXElements.rb +0 -2
- data/lib/accessibility/core.rb +180 -123
- data/lib/accessibility/dsl.rb +310 -191
- data/lib/accessibility/enumerators.rb +9 -8
- data/lib/accessibility/errors.rb +7 -8
- data/lib/accessibility/factory.rb +16 -9
- data/lib/accessibility/graph.rb +68 -22
- data/lib/accessibility/highlighter.rb +86 -0
- data/lib/accessibility/pp_inspector.rb +4 -4
- data/lib/accessibility/qualifier.rb +11 -9
- data/lib/accessibility/string.rb +12 -4
- data/lib/accessibility/translator.rb +19 -10
- data/lib/accessibility/version.rb +3 -1
- data/lib/accessibility.rb +42 -17
- data/lib/ax/application.rb +90 -30
- data/lib/ax/button.rb +5 -2
- data/lib/ax/element.rb +133 -149
- data/lib/ax/pop_up_button.rb +12 -0
- data/lib/ax/radio_button.rb +5 -2
- data/lib/ax/row.rb +2 -2
- data/lib/ax/static_text.rb +5 -2
- data/lib/ax/systemwide.rb +24 -12
- data/lib/ax_elements/awesome_print.rb +13 -0
- data/lib/ax_elements/exception_workaround.rb +5 -0
- data/lib/ax_elements/nsarray_compat.rb +1 -0
- data/lib/ax_elements.rb +2 -1
- data/lib/minitest/ax_elements.rb +60 -4
- data/lib/mouse.rb +47 -20
- data/lib/rspec/expectations/ax_elements.rb +180 -88
- data/rakelib/doc.rake +7 -0
- data/test/helper.rb +2 -1
- data/test/integration/accessibility/test_dsl.rb +126 -18
- data/test/integration/accessibility/test_errors.rb +1 -1
- data/test/integration/ax/test_element.rb +17 -0
- data/test/integration/minitest/test_ax_elements.rb +33 -38
- data/test/integration/rspec/expectations/test_ax_elements.rb +68 -19
- data/test/sanity/accessibility/test_core.rb +45 -37
- data/test/sanity/accessibility/test_highlighter.rb +56 -0
- data/test/sanity/ax/test_application.rb +8 -0
- data/test/sanity/ax/test_element.rb +7 -3
- data/test/sanity/minitest/test_ax_elements.rb +2 -0
- data/test/sanity/rspec/expectations/test_ax_elements.rb +3 -0
- data/test/sanity/test_accessibility.rb +9 -0
- data/test/sanity/test_mouse.rb +2 -2
- metadata +11 -38
- data/docs/AccessibilityTips.markdown +0 -119
- data/docs/Acting.markdown +0 -340
- data/docs/Debugging.markdown +0 -165
- data/docs/Inspecting.markdown +0 -261
- data/docs/KeyboardEvents.markdown +0 -122
- data/docs/NewBehaviour.markdown +0 -151
- data/docs/Notifications.markdown +0 -271
- data/docs/Searching.markdown +0 -250
- data/docs/TestingExtensions.markdown +0 -52
- data/docs/images/all_the_buttons.jpg +0 -0
- data/docs/images/next_version.png +0 -0
- data/docs/images/ui_hierarchy.dot +0 -34
- data/docs/images/ui_hierarchy.png +0 -0
- data/lib/accessibility/debug.rb +0 -164
- data/test/integration/accessibility/test_debug.rb +0 -44
- data/test/sanity/accessibility/test_debug.rb +0 -63
@@ -88,4 +88,12 @@ class TestAXApplication < MiniTest::Unit::TestCase
|
|
88
88
|
assert_equal running_app.bundleIdentifier, app.bundle_identifier
|
89
89
|
end
|
90
90
|
|
91
|
+
def test_info_plist
|
92
|
+
assert_equal 'hmmmmm.icns', app.info_plist['CFBundleIconFile']
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_version
|
96
|
+
assert_equal '1.0', app.version
|
97
|
+
end
|
98
|
+
|
91
99
|
end
|
@@ -7,6 +7,10 @@ class TestAXElement < MiniTest::Unit::TestCase
|
|
7
7
|
@element ||= AX::Element.new REF
|
8
8
|
end
|
9
9
|
|
10
|
+
def test_ancestry
|
11
|
+
assert_equal [element], element.ancestry
|
12
|
+
end
|
13
|
+
|
10
14
|
def test_children # not much we can do here
|
11
15
|
assert_respond_to element, :children
|
12
16
|
assert_kind_of Array, element.children
|
@@ -38,12 +42,12 @@ class TestAXElement < MiniTest::Unit::TestCase
|
|
38
42
|
def center_test position, size, expected
|
39
43
|
element.define_singleton_method :attribute do |attr|
|
40
44
|
case attr
|
41
|
-
when :position then
|
42
|
-
when :size then
|
45
|
+
when :position then CGPoint.new(*position)
|
46
|
+
when :size then CGSize.new(*size)
|
43
47
|
else raise ArgumentError
|
44
48
|
end
|
45
49
|
end
|
46
|
-
assert_equal
|
50
|
+
assert_equal CGPoint.new(*expected), element.to_point
|
47
51
|
end
|
48
52
|
|
49
53
|
# the nil case
|
@@ -7,9 +7,11 @@ class TestMiniTestAssertions < MiniTest::Unit::TestCase
|
|
7
7
|
assert_respond_to self, :assert_has_child
|
8
8
|
assert_respond_to self, :assert_has_descendent
|
9
9
|
assert_respond_to self, :assert_has_descendant
|
10
|
+
assert_respond_to self, :assert_shortly_has
|
10
11
|
assert_respond_to self, :refute_has_child
|
11
12
|
assert_respond_to self, :refute_has_descendent
|
12
13
|
assert_respond_to self, :refute_has_descendant
|
14
|
+
assert_respond_to self, :refute_shortly_has
|
13
15
|
end
|
14
16
|
|
15
17
|
end
|
@@ -7,6 +7,9 @@ class TestRSpecMatchers < MiniTest::Unit::TestCase
|
|
7
7
|
assert TopLevel.method(:have_child)
|
8
8
|
assert TopLevel.method(:have_descendent)
|
9
9
|
assert TopLevel.method(:have_descendant)
|
10
|
+
assert TopLevel.method(:shortly_have_child)
|
11
|
+
assert TopLevel.method(:shortly_have_descendent)
|
12
|
+
assert TopLevel.method(:shortly_have_descendant)
|
10
13
|
end
|
11
14
|
|
12
15
|
end
|
data/test/sanity/test_mouse.rb
CHANGED
@@ -7,11 +7,11 @@ class TestMouseModule < MiniTest::Unit::TestCase
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def test_move_to
|
10
|
-
point =
|
10
|
+
point = CGPoint.new(100, 100)
|
11
11
|
Mouse.move_to point
|
12
12
|
assert_in_delta 0, distance(point,Mouse.current_position), 1.0
|
13
13
|
|
14
|
-
point =
|
14
|
+
point = CGPoint.new(rand(700)+150, rand(500)+100)
|
15
15
|
Mouse.move_to point
|
16
16
|
assert_in_delta 0, distance(point,Mouse.current_position), 1.0
|
17
17
|
end
|
metadata
CHANGED
@@ -2,14 +2,14 @@
|
|
2
2
|
name: AXElements
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.
|
5
|
+
version: 0.8.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Mark Rada
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-04-
|
12
|
+
date: 2012-04-24 00:00:00 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: minitest
|
@@ -70,30 +70,15 @@ email: mrada@marketcircle.com
|
|
70
70
|
executables: []
|
71
71
|
extensions:
|
72
72
|
- ext/accessibility/key_coder/extconf.rb
|
73
|
-
extra_rdoc_files:
|
74
|
-
- docs/AccessibilityTips.markdown
|
75
|
-
- docs/Acting.markdown
|
76
|
-
- docs/Debugging.markdown
|
77
|
-
- docs/images/all_the_buttons.jpg
|
78
|
-
- docs/images/next_version.png
|
79
|
-
- docs/images/ui_hierarchy.dot
|
80
|
-
- docs/images/ui_hierarchy.png
|
81
|
-
- docs/Inspecting.markdown
|
82
|
-
- docs/KeyboardEvents.markdown
|
83
|
-
- docs/NewBehaviour.markdown
|
84
|
-
- docs/Notifications.markdown
|
85
|
-
- docs/Searching.markdown
|
86
|
-
- docs/TestingExtensions.markdown
|
87
|
-
- .yardopts
|
88
|
-
- README.markdown
|
73
|
+
extra_rdoc_files: []
|
89
74
|
files:
|
90
75
|
- lib/accessibility/core.rb
|
91
|
-
- lib/accessibility/debug.rb
|
92
76
|
- lib/accessibility/dsl.rb
|
93
77
|
- lib/accessibility/enumerators.rb
|
94
78
|
- lib/accessibility/errors.rb
|
95
79
|
- lib/accessibility/factory.rb
|
96
80
|
- lib/accessibility/graph.rb
|
81
|
+
- lib/accessibility/highlighter.rb
|
97
82
|
- lib/accessibility/pp_inspector.rb
|
98
83
|
- lib/accessibility/qualifier.rb
|
99
84
|
- lib/accessibility/string.rb
|
@@ -103,6 +88,7 @@ files:
|
|
103
88
|
- lib/ax/application.rb
|
104
89
|
- lib/ax/button.rb
|
105
90
|
- lib/ax/element.rb
|
91
|
+
- lib/ax/pop_up_button.rb
|
106
92
|
- lib/ax/radio_button.rb
|
107
93
|
- lib/ax/row.rb
|
108
94
|
- lib/ax/static_text.rb
|
@@ -125,8 +111,9 @@ files:
|
|
125
111
|
- rakelib/gem.rake
|
126
112
|
- rakelib/test.rake
|
127
113
|
- Rakefile
|
114
|
+
- README.markdown
|
115
|
+
- .yardopts
|
128
116
|
- test/integration/accessibility/test_core.rb
|
129
|
-
- test/integration/accessibility/test_debug.rb
|
130
117
|
- test/integration/accessibility/test_dsl.rb
|
131
118
|
- test/integration/accessibility/test_enumerators.rb
|
132
119
|
- test/integration/accessibility/test_errors.rb
|
@@ -138,10 +125,10 @@ files:
|
|
138
125
|
- test/integration/minitest/test_ax_elements.rb
|
139
126
|
- test/integration/rspec/expectations/test_ax_elements.rb
|
140
127
|
- test/sanity/accessibility/test_core.rb
|
141
|
-
- test/sanity/accessibility/test_debug.rb
|
142
128
|
- test/sanity/accessibility/test_dsl.rb
|
143
129
|
- test/sanity/accessibility/test_errors.rb
|
144
130
|
- test/sanity/accessibility/test_factory.rb
|
131
|
+
- test/sanity/accessibility/test_highlighter.rb
|
145
132
|
- test/sanity/accessibility/test_pp_inspector.rb
|
146
133
|
- test/sanity/accessibility/test_qualifier.rb
|
147
134
|
- test/sanity/accessibility/test_string.rb
|
@@ -153,24 +140,10 @@ files:
|
|
153
140
|
- test/sanity/ax_elements/test_nsobject_inspect.rb
|
154
141
|
- test/sanity/minitest/test_ax_elements.rb
|
155
142
|
- test/sanity/rspec/expectations/test_ax_elements.rb
|
143
|
+
- test/sanity/test_accessibility.rb
|
156
144
|
- test/sanity/test_ax_elements.rb
|
157
145
|
- test/sanity/test_mouse.rb
|
158
146
|
- test/helper.rb
|
159
|
-
- docs/AccessibilityTips.markdown
|
160
|
-
- docs/Acting.markdown
|
161
|
-
- docs/Debugging.markdown
|
162
|
-
- docs/images/all_the_buttons.jpg
|
163
|
-
- docs/images/next_version.png
|
164
|
-
- docs/images/ui_hierarchy.dot
|
165
|
-
- docs/images/ui_hierarchy.png
|
166
|
-
- docs/Inspecting.markdown
|
167
|
-
- docs/KeyboardEvents.markdown
|
168
|
-
- docs/NewBehaviour.markdown
|
169
|
-
- docs/Notifications.markdown
|
170
|
-
- docs/Searching.markdown
|
171
|
-
- docs/TestingExtensions.markdown
|
172
|
-
- .yardopts
|
173
|
-
- README.markdown
|
174
147
|
homepage: http://github.com/Marketcircle/AXElements
|
175
148
|
licenses:
|
176
149
|
- BSD 3-clause
|
@@ -198,7 +171,6 @@ specification_version: 3
|
|
198
171
|
summary: A DSL for automating GUI manipulation
|
199
172
|
test_files:
|
200
173
|
- test/integration/accessibility/test_core.rb
|
201
|
-
- test/integration/accessibility/test_debug.rb
|
202
174
|
- test/integration/accessibility/test_dsl.rb
|
203
175
|
- test/integration/accessibility/test_enumerators.rb
|
204
176
|
- test/integration/accessibility/test_errors.rb
|
@@ -210,10 +182,10 @@ test_files:
|
|
210
182
|
- test/integration/minitest/test_ax_elements.rb
|
211
183
|
- test/integration/rspec/expectations/test_ax_elements.rb
|
212
184
|
- test/sanity/accessibility/test_core.rb
|
213
|
-
- test/sanity/accessibility/test_debug.rb
|
214
185
|
- test/sanity/accessibility/test_dsl.rb
|
215
186
|
- test/sanity/accessibility/test_errors.rb
|
216
187
|
- test/sanity/accessibility/test_factory.rb
|
188
|
+
- test/sanity/accessibility/test_highlighter.rb
|
217
189
|
- test/sanity/accessibility/test_pp_inspector.rb
|
218
190
|
- test/sanity/accessibility/test_qualifier.rb
|
219
191
|
- test/sanity/accessibility/test_string.rb
|
@@ -225,6 +197,7 @@ test_files:
|
|
225
197
|
- test/sanity/ax_elements/test_nsobject_inspect.rb
|
226
198
|
- test/sanity/minitest/test_ax_elements.rb
|
227
199
|
- test/sanity/rspec/expectations/test_ax_elements.rb
|
200
|
+
- test/sanity/test_accessibility.rb
|
228
201
|
- test/sanity/test_ax_elements.rb
|
229
202
|
- test/sanity/test_mouse.rb
|
230
203
|
- test/helper.rb
|
@@ -1,119 +0,0 @@
|
|
1
|
-
# Accessibility Tips
|
2
|
-
|
3
|
-
This document includes tips for customizing accessibility in your own
|
4
|
-
applications. The goal is to inform you of pitfalls that are not
|
5
|
-
mentioned in Apple's documentation. It also includes notes on how to
|
6
|
-
avoid making decsions that will make an application incompatible with
|
7
|
-
AXElements.
|
8
|
-
|
9
|
-
@todo This document is under construction and is currently just a set
|
10
|
-
of notes and unorganized sections ripped from other documents.
|
11
|
-
|
12
|
-
## Guidelines
|
13
|
-
|
14
|
-
When implementing custom accessibility roles, you should never use a
|
15
|
-
pluralized name for the role. A role that is already pluralized will
|
16
|
-
break search.
|
17
|
-
|
18
|
-
|
19
|
-
## Adding new stuff
|
20
|
-
|
21
|
-
Adding new attributes to an object is very simple...usually. Most of
|
22
|
-
the time it is as simple as overriding two methods:
|
23
|
-
|
24
|
-
- `accessibilityAttributeNames`
|
25
|
-
- `accessibilityAttributeValue:`
|
26
|
-
|
27
|
-
`accessibilityAttributeNames` needs to return an array with the names
|
28
|
-
of available attributes. You should call `super` to get the existing
|
29
|
-
array first, and then append any custom attributes you want to
|
30
|
-
provide.
|
31
|
-
|
32
|
-
An attribute name must follow the convention of being a camel cased
|
33
|
-
string with the "AX" prefix, such as `AXTitle`. You can optionally
|
34
|
-
include an additional prefix before "AX", such as "MCAX" for
|
35
|
-
Marketcircle custom attributes. If you do not follow these rules then
|
36
|
-
attribute names will not be translated properly by AXElements.
|
37
|
-
|
38
|
-
`accessibilityAttributeValue:` is how you actually provide the value
|
39
|
-
for the attribute; the parameter for this method is the name of the
|
40
|
-
attribute fetched from calling `accessibilityAttributeNames`. You
|
41
|
-
should return the value without doing any extra work to the data; the
|
42
|
-
Accessibility interface will do any wrapping or translating for
|
43
|
-
you (e.g. CGPoint objects will have co-ordinates flipped). In the case
|
44
|
-
of a C structs you should leave them wrapped in an NSValue object.
|
45
|
-
|
46
|
-
Similarly, parameterized attributes are added using methods with
|
47
|
-
`Parameter` in the name. The method for getting the value of a
|
48
|
-
parameterized attribute will of course take an extra parameter for the
|
49
|
-
parameterized attributes parameter.
|
50
|
-
|
51
|
-
### Where To Implement The Methods
|
52
|
-
|
53
|
-
The difficulty in adding attributes lies in finding out where you need
|
54
|
-
to add the accessibility methods. Often, the class that implements
|
55
|
-
accessibility is the class that draws the user interface element. For
|
56
|
-
example, the accessibility information for a button is implemented on
|
57
|
-
the button cell and not the button itself. Since Apple is under the
|
58
|
-
delusion that you will never need to add any custom accessibility
|
59
|
-
information, they really don't document these customizations enough,
|
60
|
-
and so this document only includes caveats that have been discovered
|
61
|
-
so far.
|
62
|
-
|
63
|
-
It is often inconvenient to subclass the cell for an object just for
|
64
|
-
accessibility. In these cases Apple has provided something similar to
|
65
|
-
singleton methods, but less useful, with the
|
66
|
-
`accessibilitySetOverrideValue:forAttribute:` method. You can use the
|
67
|
-
method to override an attribute or even add a new one. The main issue
|
68
|
-
with this method is that any attribute that is overridden, or added,
|
69
|
-
will not be writable using the accessibility APIs. Another issue is
|
70
|
-
that you have to calculate the value when you override instead of when
|
71
|
-
the attribute value is queried by the client.
|
72
|
-
|
73
|
-
### Writability
|
74
|
-
|
75
|
-
In the case where it is convenient to be able to change an attribute's
|
76
|
-
value through accessibility, like the size of a window, you will also
|
77
|
-
need to implement two more methods for the attribute:
|
78
|
-
|
79
|
-
- `accessibilityIsAttributeSettable:`
|
80
|
-
- `accessibilitySetValue:forAttribute:`
|
81
|
-
|
82
|
-
`accessibilityIsAttributeSettable:` simply responds with whether or
|
83
|
-
not the attribute is writable, and
|
84
|
-
`accessibilitySetValue:forAttribute:` will be called to actually write
|
85
|
-
to the attribute.
|
86
|
-
|
87
|
-
### Remember `super`
|
88
|
-
|
89
|
-
It is important to remember that these methods are already implemented
|
90
|
-
for the built in features. When overriding, you should only implement
|
91
|
-
the custom behaviour and call `super` to handle everything else.
|
92
|
-
|
93
|
-
### Existing Definitions
|
94
|
-
|
95
|
-
When implementing custom behaviour, you should try and use
|
96
|
-
pseudoclasses that have already been defined by Apple, as well as
|
97
|
-
attributes, parameterized attributes, and other features that have
|
98
|
-
already been defined. Apple maintains the documentation for all their
|
99
|
-
definitions on
|
100
|
-
[here](http://developer.apple.com/library/mac/#documentation/UserExperience/Reference/Accessibility_RoleAttribute_Ref/Introduction.html#//apple_ref/doc/uid/TP40007870).
|
101
|
-
The documentation is fairly detailed now (moreso than before), but
|
102
|
-
still misses a few things.
|
103
|
-
|
104
|
-
|
105
|
-
## Adding Accessibility Actions
|
106
|
-
|
107
|
-
If you have access to the source code for an app, you can add more
|
108
|
-
accessibility actions. You need to override two methods, just like
|
109
|
-
with adding attributes:
|
110
|
-
|
111
|
-
– `accessibilityActionNames`
|
112
|
-
– `accessibilityPerformAction:`
|
113
|
-
|
114
|
-
These methods should be implemented in the same way that you would
|
115
|
-
implement new attributes, just as detailed in the
|
116
|
-
{file:docs/Inspecting.markdown Inspecting tutorial}. The one
|
117
|
-
difference is that `accessibilityPerformAction:` should not return
|
118
|
-
anything after it performs the action.
|
119
|
-
|
data/docs/Acting.markdown
DELETED
@@ -1,340 +0,0 @@
|
|
1
|
-
# Actions (a.k.a. The Fun Part)
|
2
|
-
|
3
|
-
Actions are how you drive the user interface. They can be anything
|
4
|
-
from showing a menu to dragging a window, but are often simple things
|
5
|
-
like pressing a button. Most actions are subtly different cases
|
6
|
-
of the same thing, but it is important to understand the differences
|
7
|
-
and when to use different actions.
|
8
|
-
|
9
|
-
There are three types of actions, those provided by the accessibility
|
10
|
-
API as direct calls, those provided by accessibility APIs as keyboard
|
11
|
-
simulation, and those that are provided by the CGEvents API. In most
|
12
|
-
cases you can use at least two types of actions to achieve the desired
|
13
|
-
effect and each type of action has its own pros and cons to consider.
|
14
|
-
|
15
|
-
## Interface
|
16
|
-
|
17
|
-
Though there are three different APIs under the hood, they are exposed
|
18
|
-
through a common interface via the DSL methods in
|
19
|
-
{Accessibility::Language}.
|
20
|
-
|
21
|
-
app = Accessibility.application_with_name 'Terminal'
|
22
|
-
press app.main_window.minimize_button
|
23
|
-
type "\\CMD+M", app
|
24
|
-
click app.main_window.minimize_button
|
25
|
-
|
26
|
-
The above example demonstrates performing actions using the DSL
|
27
|
-
methods. The DSL methods are always some kind of action, such as
|
28
|
-
{Accessibility::Language#drag_mouse_to drag\_mouse\_to}, or
|
29
|
-
{Accessibility::Language#set_focus set\_focus} to name two.
|
30
|
-
|
31
|
-
The point of the DSL is to put the actions at the front of statements
|
32
|
-
in order to separate verbs from nouns. In this way, scripts should
|
33
|
-
resemble instructions that you might give for reproduction steps in a
|
34
|
-
bug report---they should read like they were instructions for
|
35
|
-
a person. This style should make it easier to think about writing
|
36
|
-
scripts and it should also give better hints at when to refactor
|
37
|
-
code into helper methods like `log_in_as:with_password:`.
|
38
|
-
|
39
|
-
If you tried out the code snippet above you might notice the
|
40
|
-
difference between the APIs (you also might notice that the second
|
41
|
-
command does not work, it is a work in progress :(). In the first
|
42
|
-
case, `press` was using the actions provided by the accessibility APIs
|
43
|
-
to call the press action on the minimize button for the window, in the
|
44
|
-
second case we used `type` to type in the hot key for minimizing a
|
45
|
-
window, and in the third case we used `click` from the CGEvents APIs
|
46
|
-
to actually move the mouse cursor to the button and generate a click
|
47
|
-
event.
|
48
|
-
|
49
|
-
## Accessibility Actions
|
50
|
-
|
51
|
-
Actions like `press` use the accessibility APIs and show up in the
|
52
|
-
list of actions returned by {AX::Element#actions}. These actions will
|
53
|
-
also show up in the Accessibility Inspector in the Actions section.
|
54
|
-
|
55
|
-
You may have noticed that {Accessibility::Language#press press}
|
56
|
-
shows up as `AXPress` for buttons when you view them with the
|
57
|
-
Accessibility Inspector or by calling {AX::Element#actions}. This is
|
58
|
-
because we do name translation with accessibility actions just like
|
59
|
-
when accessing attributes as shown in the
|
60
|
-
{file:docs/Inspecting.markdown Inspection tutorial}.
|
61
|
-
|
62
|
-
Accessibility actions are the simplest and fastest way to trigger
|
63
|
-
actions. They use the accessibility APIs to talk directly to the
|
64
|
-
application to call the same methods that would be called when
|
65
|
-
you interact with the application with the mouse or keyboard, but
|
66
|
-
without having to use the mouse or keyboard.
|
67
|
-
|
68
|
-
## Setting Attributes
|
69
|
-
|
70
|
-
Though it is not really an action, some attributes can have their
|
71
|
-
values changed through the accessibility APIs. The
|
72
|
-
{Accessibility::Language} module has two methods, `set` and
|
73
|
-
`set_focus`, which allow you to change the value for certain
|
74
|
-
attributes.
|
75
|
-
|
76
|
-
In the Accessibility Inspector, a writable attribute will have `(W)`
|
77
|
-
next to the attribute name, and the programmatic way to tell if an
|
78
|
-
attribute is writable is to call {AX::Element#attribute_writable?} and
|
79
|
-
pass the name of the attribute as a parameter:
|
80
|
-
|
81
|
-
app.main_window.attribute_writable? :size
|
82
|
-
app.main_window.attribute_writable? :title
|
83
|
-
|
84
|
-
You can only set an attribute if it is writable. When you have an
|
85
|
-
attribute that is writable, and you would like to change it, then you
|
86
|
-
simply need to call `set`:
|
87
|
-
|
88
|
-
set app.main_window, size: [500, 500].to_size
|
89
|
-
|
90
|
-
The first parameter is the element, and the second parameter is a
|
91
|
-
key-value pair with the attribute as the key and the value as the
|
92
|
-
new value.
|
93
|
-
|
94
|
-
`set_focus` is just syntactic sugar for `set` where the key-value is
|
95
|
-
set for you and the only parameter you provide is the element you want
|
96
|
-
to set focus to:
|
97
|
-
|
98
|
-
set_focus app.main_window.text_field
|
99
|
-
|
100
|
-
Though, `set` itself has a special case. The second most frequently
|
101
|
-
changed attribute for a element is the `value`, it is used almost as
|
102
|
-
much as `focused`. For this case, you can pass anything that is not a
|
103
|
-
key-value pair to `set` and it will be assumed that you want to change
|
104
|
-
the value of the `value` attribute:
|
105
|
-
|
106
|
-
set app.main_window.text_field, 'Mark Rada'
|
107
|
-
|
108
|
-
Another important detail that you might be curious about is that we
|
109
|
-
called `to_size` on the first snippet showing how to set an
|
110
|
-
attribute. The developers of MacRuby have added a feature that allows
|
111
|
-
you to pass an array as an argument when you would normally be
|
112
|
-
expected to pass a structure such as a `CGPoint`. Since AXElements
|
113
|
-
APIs inherently need to handle any type of object, it is not sane to try
|
114
|
-
and do the same type of run time type analysis. In its place, I have
|
115
|
-
provided some convenience methods for {NSArray arrays} to let you
|
116
|
-
quickly transform them to the appropriate type of object. In my
|
117
|
-
opinion, you should just use the proper type of object in the first
|
118
|
-
place and avoid the overhead.
|
119
|
-
|
120
|
-
### You Want To Use Actions
|
121
|
-
|
122
|
-
The advantage to using accessibility actions is that they are usually
|
123
|
-
much easier to use than the other APIs. Accessibility actions are
|
124
|
-
usually synchronous and so your scripts will not need to worry about
|
125
|
-
waiting for animations to complete. The only complication is that
|
126
|
-
sometimes an action is not synchronous and there is no good way to
|
127
|
-
tell if an action is synchronous without looking at the underlying
|
128
|
-
code.
|
129
|
-
|
130
|
-
### Asynchronous Problems
|
131
|
-
|
132
|
-
Problems with synchronous behaviour will occur with all the types of
|
133
|
-
actions, but less often with accessibility actions, so you should
|
134
|
-
always try thing out first in the console to make sure. A good rule of
|
135
|
-
thumb is that anything that looks like it would take a noticeable
|
136
|
-
amount of time, and is not an animation, will probably be
|
137
|
-
asynchronous. An example of this would be having to load data from a
|
138
|
-
database (read: separate process, across a network, etc.) such as in
|
139
|
-
the Marketcircle server admin apps or the Billings Pro preferences.
|
140
|
-
|
141
|
-
Asynchronous behaviour can often be worked around by using
|
142
|
-
accessibility {file:docs/Notifications.markdown notifications} or by
|
143
|
-
simply calling
|
144
|
-
[`sleep`](http://rdoc.info/stdlib/core/1.9.2/Kernel#sleep-instance_method).
|
145
|
-
Notifications have some interesting caveats that are covered in their
|
146
|
-
own tutorial and so it is much easier to `sleep`.
|
147
|
-
|
148
|
-
However, using `sleep` to solve the problem of asynchronous waiting is
|
149
|
-
like using a steak knife for brain surgery. Unless you can control the
|
150
|
-
environment when the script is running, you will need to sleep for
|
151
|
-
longer periods of time than you really need to; even when you have
|
152
|
-
quite a bit of control you might still have the occasional instance
|
153
|
-
where a database fetch takes longer than expected. I would suggest you
|
154
|
-
use notifications when you can, and sleep when you cannot.
|
155
|
-
|
156
|
-
## CGEvents Actions
|
157
|
-
|
158
|
-
CGEvents based actions never directly use the accessibility APIs and
|
159
|
-
are implemented at a different level of the GUI stack in OS X (I
|
160
|
-
assume). An action that uses CGEvents will actually move the mouse
|
161
|
-
cursor around and simulate mouse input at a relatively low level of
|
162
|
-
abstraction. However, accessibility information is still used to find
|
163
|
-
the point on the screen to move to and click. These types of actions
|
164
|
-
are more realistic, more awesome looking, and also more difficult to
|
165
|
-
write.
|
166
|
-
|
167
|
-
The difficulty in writing the scripts comes from the fact that it does
|
168
|
-
not directly communicate with the accessibility APIs. This implicitly
|
169
|
-
means that all CGEvents actions are asynchronous, which is the only
|
170
|
-
non-trivial complication with using CGEvents APIs.
|
171
|
-
|
172
|
-
### CGEvents Goes Where Accessibility Cannot
|
173
|
-
|
174
|
-
The benefit of actually moving the mouse will often outweigh the
|
175
|
-
downside here. Moving the mouse cursor and generating click events
|
176
|
-
allows things like dragging and scrolling which cannot be done using
|
177
|
-
the accessibility APIs.
|
178
|
-
|
179
|
-
As mentioned earlier, CGEvents doesn't talk to applications in the
|
180
|
-
same way that accessibility APIs do, which means that you can use
|
181
|
-
CGEvents actions on elements that do not fully support
|
182
|
-
accessibility. For instance, text might appear on the UI and have a
|
183
|
-
`click`-able link but may not provide an equivalent action to clicking
|
184
|
-
on the link; using CGEvents APIs you will only need to move to the
|
185
|
-
position of the text and then generate a click event.
|
186
|
-
|
187
|
-
### Realism
|
188
|
-
|
189
|
-
Since CGEvents actually manipulates the mouse cursor, any script that
|
190
|
-
uses the APIs will be more realistic than their equivalent
|
191
|
-
accessibility actions. This can make a difference if you are testing
|
192
|
-
an app and you are expecting other side effects to occur. For instance,
|
193
|
-
what if you implemented a sort of UI element that requires the mouse
|
194
|
-
cursor to be near the element for an action to trigger, such as an
|
195
|
-
expanding drawer or a folder in the finder. Depending on what you are
|
196
|
-
using AXElements for, this may or may not be important to you.
|
197
|
-
|
198
|
-
### Underpinnings
|
199
|
-
|
200
|
-
An important thing to note is that AXElements always works with
|
201
|
-
flipped co-ordinates. The origin, `(0, 0)` is always located in the
|
202
|
-
top left corner of the main display. Displays to the left have a
|
203
|
-
negative `x` co-ordinate and displays above the main display will have
|
204
|
-
negative `y` co-ordinates.
|
205
|
-
|
206
|
-
Though the CGEvents APIs are exposed to AXElements through the
|
207
|
-
{Accessibility::Language Language} module, there is another thin
|
208
|
-
layer between AXElements and CGEvents that abstracts everything that
|
209
|
-
AXElements uses.
|
210
|
-
|
211
|
-
The between layer is the {Mouse} module, and it is responsible for
|
212
|
-
generating events and animating the mouse movements on screen. The API
|
213
|
-
that it makes available to AXElements is more fine grained than what
|
214
|
-
AXElements makes available to you. There is a lot of room for the
|
215
|
-
{Mouse} module to grow and add new features, some of which are noted
|
216
|
-
as `@todo` items in the documentation. It may also be necessary in
|
217
|
-
the future to make available from AXElements certain options that are
|
218
|
-
normally hidden in {Mouse}.
|
219
|
-
|
220
|
-
### Where The Mouse Moves
|
221
|
-
|
222
|
-
Consider the following:
|
223
|
-
|
224
|
-
move_mouse_to app.main_window.close_button
|
225
|
-
|
226
|
-
If you try out that code snippet then you will notice that the mouse
|
227
|
-
cursor moves to the button, specifically the center of the button. If
|
228
|
-
you use the method to move to another object, such as a button, you
|
229
|
-
will again notice that it moved to the center point of the element.
|
230
|
-
|
231
|
-
You can call `move_mouse_to` and give it a UI element, but you could
|
232
|
-
also be more exact and give it a CGPoint or even an array with two
|
233
|
-
numbers. In fact, you could pass just any object as long it implements
|
234
|
-
a method named `to_point` which returns a CGPoint object. If you were
|
235
|
-
to look at the code for {Accessibility::Language#move_mouse_to} you
|
236
|
-
would see that all it does is call `#to_point` on the argument and
|
237
|
-
then pass the returned value to the {Mouse} module to actually do the
|
238
|
-
work. You could also look at {NSArray#to_point} and find out that it just
|
239
|
-
returns a new CGPoint using the first two objects in the array, and
|
240
|
-
`CGPoint#to_point` just returns itself (you can't see the
|
241
|
-
documentation for the method because of limitations in
|
242
|
-
YARD). Similarly, {AX::Element} implements
|
243
|
-
{AX::Element#to_point to_point} which not only gets the position for
|
244
|
-
the element, but also gets the size and then calculates the center
|
245
|
-
point for the element. This is important because you probably never
|
246
|
-
want the mouse cursor moving to the top left of the UI element, but
|
247
|
-
maybe you don't want the cursor moving to the center either.
|
248
|
-
|
249
|
-
If you want to have the mouse move to a location over a UI element
|
250
|
-
that is not the center point, then you will need to implement
|
251
|
-
`to_point` for the appropriate class. Just remember to follow the
|
252
|
-
{file:docs/NewBehaviour.markdown rules} for choosing the proper
|
253
|
-
superclasss. For instance, on Snow Leopard you could resize a window
|
254
|
-
by clicking and dragging an `AX::GrowArea` object in the lower right
|
255
|
-
corner of a window; moving to the center of the element may not
|
256
|
-
actually allow the mouse to respond and so you would have to implement
|
257
|
-
`to_point` to move closer to the bottom right of the window
|
258
|
-
(__NOTE__: I don't know if that is actually true for `AX::GrowArea`,
|
259
|
-
it was just meant as an example).
|
260
|
-
|
261
|
-
### Dragging
|
262
|
-
|
263
|
-
Sometimes you just need to be able to click and drag an element. To do
|
264
|
-
this you can only use the CGEvents APIs. Fortunately, click and drag
|
265
|
-
is relatively painless with AXElements, the simplest example would be
|
266
|
-
something like this:
|
267
|
-
|
268
|
-
# move to the mouse to the starting point
|
269
|
-
move_mouse_to app.main_window.title_ui_element
|
270
|
-
|
271
|
-
# start the click and drag event
|
272
|
-
drag_mouse_to [0, 0]
|
273
|
-
|
274
|
-
Pretty cool, eh? The general pattern is to move the mouse to the
|
275
|
-
starting point, and then to call `drag_mouse_to` to start the click
|
276
|
-
and drag events. In the example, I gave co-ordinates using an array
|
277
|
-
with two numbers but you can pass a CGPoint or anything else that
|
278
|
-
responds to `to_point` just like with the other CGEvents actions.
|
279
|
-
|
280
|
-
## Keyboard Actions
|
281
|
-
|
282
|
-
The final type of action is the keyboard action. Keyboard actions
|
283
|
-
live in a world between the accessibility APIs and CGEvent APIs; that
|
284
|
-
is, Apple has already done the work of unifying them for me and in a
|
285
|
-
way that I would not have been able to do myself.
|
286
|
-
|
287
|
-
The keyboard action is a single method,
|
288
|
-
{Accessibility::Language#type}, that requires one parameter and
|
289
|
-
takes an optional second parameter. The first parameter is simply a
|
290
|
-
string that includes which characters you wish to have typed out and
|
291
|
-
the second parameter is the UI element for the application where you
|
292
|
-
would like to send the keyboard events. Since the second parameter is
|
293
|
-
optional, the currently focused app will receive the events if the
|
294
|
-
parameter is not used.
|
295
|
-
|
296
|
-
# to a specific app
|
297
|
-
app = Accessibility.application_with_name 'Terminal'
|
298
|
-
type "Hello, world!", app
|
299
|
-
|
300
|
-
# to the focused app
|
301
|
-
type "Hello, world!"
|
302
|
-
|
303
|
-
The string that you pass can have some special escape sequences to
|
304
|
-
indicate that you want to press command characters or a combination of
|
305
|
-
keys at once for a hot key. Details on this are contained in their own
|
306
|
-
tutorial, the
|
307
|
-
{file:docs/KeyboardEvents.markdown Keyboard Events Tutorial}.
|
308
|
-
|
309
|
-
## Mixing In
|
310
|
-
|
311
|
-
By now you will have noticed that all the actions have been defined in
|
312
|
-
the {Accessibility::Language} name space, which is just a simple
|
313
|
-
module. Though, you have been able to use the methods anywhere and in
|
314
|
-
any context.
|
315
|
-
|
316
|
-
When you load AXElements, by using `require 'ax_elements'`, not only
|
317
|
-
will all the code be loaded, but the extra step of mixing
|
318
|
-
{Accessibility::Language} into the top level name space will also be
|
319
|
-
done for you. The only way to override this behaviour is to load the
|
320
|
-
components of AXElements yourself, the details on how to do this are
|
321
|
-
left as an exercise to interested parties. :P
|
322
|
-
|
323
|
-
## A Note About Caching
|
324
|
-
|
325
|
-
You need to be careful when you cache UI element objects. Every time
|
326
|
-
that you trigger an action you are intrinsically changing the state of
|
327
|
-
an application. State changes will often cause new UI elements to be
|
328
|
-
created, recreated, or removed.
|
329
|
-
|
330
|
-
For example consider pressing the close button for a window; in this
|
331
|
-
case, an entire window and all of its children will disappear and
|
332
|
-
become invalid objects. Another case might be pressing the `+` button
|
333
|
-
for a table; in this case you have created a new row for the table and
|
334
|
-
any cache of the existing rows for the table will not include the new
|
335
|
-
element.
|
336
|
-
|
337
|
-
The real problem with caching is with the invalid objects. An invalid
|
338
|
-
object is poisonous to the MacRuby run time. If you try to access an
|
339
|
-
attribute or trigger a search from an invalid then you will cause
|
340
|
-
MacRuby to crash.
|