AXElements 0.6.0beta2 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.yardopts +1 -2
- data/README.markdown +152 -88
- data/Rakefile +8 -103
- data/docs/Debugging.markdown +9 -2
- data/docs/KeyboardEvents.markdown +114 -49
- data/docs/Setting.markdown +1 -0
- data/docs/images/next_version.png +0 -0
- data/ext/accessibility/key_coder/extconf.rb +22 -0
- data/ext/accessibility/key_coder/key_coder.c +113 -0
- data/lib/AXElements.rb +2 -0
- data/lib/accessibility/core.rb +897 -0
- data/lib/accessibility/debug.rb +168 -0
- data/lib/accessibility/dsl.rb +697 -0
- data/lib/accessibility/enumerators.rb +104 -0
- data/lib/accessibility/errors.rb +32 -0
- data/lib/accessibility/factory.rb +153 -0
- data/lib/accessibility/graph.rb +150 -0
- data/lib/{ax_elements/inspector.rb → accessibility/pp_inspector.rb} +39 -28
- data/lib/accessibility/qualifier.rb +158 -0
- data/lib/accessibility/string.rb +494 -0
- data/lib/accessibility/translator.rb +178 -0
- data/lib/accessibility/version.rb +7 -0
- data/lib/accessibility.rb +79 -0
- data/lib/ax/application.rb +234 -0
- data/lib/{ax_elements/elements → ax}/button.rb +2 -0
- data/lib/ax/element.rb +518 -0
- data/lib/{ax_elements/elements → ax}/radio_button.rb +2 -0
- data/lib/ax/row.rb +37 -0
- data/lib/{ax_elements/elements → ax}/static_text.rb +2 -0
- data/lib/ax/systemwide.rb +86 -0
- data/lib/ax_elements/awesome_print.rb +25 -0
- data/lib/ax_elements/exception_workaround.rb +8 -0
- data/lib/ax_elements/nsarray_compat.rb +64 -0
- data/lib/ax_elements/vendor/inflection_data.rb +65 -0
- data/lib/ax_elements/vendor/inflections.rb +172 -0
- data/lib/ax_elements/vendor/inflector.rb +306 -0
- data/lib/ax_elements.rb +14 -25
- data/lib/minitest/ax_elements.rb +112 -12
- data/lib/mouse.rb +72 -46
- data/lib/rspec/expectations/ax_elements.rb +133 -6
- data/rakelib/doc.rake +13 -0
- data/rakelib/ext.rake +61 -0
- data/rakelib/gem.rake +28 -0
- data/rakelib/test.rake +53 -0
- data/test/helper.rb +11 -97
- data/test/integration/accessibility/test_core.rb +18 -0
- data/test/integration/accessibility/test_debug.rb +44 -0
- data/test/integration/accessibility/test_dsl.rb +225 -0
- data/test/integration/accessibility/test_enumerators.rb +122 -0
- data/test/integration/accessibility/test_errors.rb +38 -0
- data/test/integration/accessibility/test_notifications.rb +22 -0
- data/test/integration/accessibility/test_qualifier.rb +148 -0
- data/test/integration/ax/test_application.rb +56 -0
- data/test/integration/ax/test_element.rb +46 -0
- data/test/integration/ax/test_row.rb +23 -0
- data/test/integration/ax_elements/test_nsarray_compat.rb +43 -0
- data/test/integration/minitest/test_ax_elements.rb +98 -0
- data/test/integration/rspec/expectations/test_ax_elements.rb +58 -0
- data/test/integration/test_mouse.rb +35 -0
- data/test/sanity/accessibility/test_core.rb +553 -0
- data/test/sanity/accessibility/test_debug.rb +63 -0
- data/test/sanity/accessibility/test_dsl.rb +75 -0
- data/test/sanity/accessibility/test_errors.rb +10 -0
- data/test/sanity/accessibility/test_factory.rb +88 -0
- data/test/sanity/accessibility/test_pp_inspector.rb +110 -0
- data/test/sanity/accessibility/test_qualifier.rb +13 -0
- data/test/sanity/accessibility/test_string.rb +238 -0
- data/test/sanity/accessibility/test_translator.rb +145 -0
- data/test/sanity/ax/test_application.rb +90 -0
- data/test/sanity/ax/test_element.rb +80 -0
- data/test/sanity/ax/test_systemwide.rb +66 -0
- data/test/sanity/ax_elements/test_nsarray_compat.rb +16 -0
- data/test/sanity/ax_elements/test_nsobject_inspect.rb +11 -0
- data/test/sanity/minitest/test_ax_elements.rb +15 -0
- data/test/sanity/rspec/expectations/test_ax_elements.rb +12 -0
- data/test/sanity/test_ax_elements.rb +10 -0
- data/test/sanity/test_mouse.rb +19 -0
- metadata +111 -93
- data/LICENSE.txt +0 -25
- data/ext/key_coder/extconf.rb +0 -6
- data/ext/key_coder/key_coder.m +0 -77
- data/lib/ax_elements/accessibility/enumerators.rb +0 -104
- data/lib/ax_elements/accessibility/graph.rb +0 -118
- data/lib/ax_elements/accessibility/language.rb +0 -347
- data/lib/ax_elements/accessibility/qualifier.rb +0 -73
- data/lib/ax_elements/accessibility.rb +0 -166
- data/lib/ax_elements/core.rb +0 -541
- data/lib/ax_elements/element.rb +0 -593
- data/lib/ax_elements/elements/application.rb +0 -88
- data/lib/ax_elements/elements/row.rb +0 -30
- data/lib/ax_elements/elements/systemwide.rb +0 -46
- data/lib/ax_elements/macruby_extensions.rb +0 -255
- data/lib/ax_elements/notification.rb +0 -37
- data/lib/ax_elements/version.rb +0 -9
- data/test/elements/test_application.rb +0 -72
- data/test/elements/test_row.rb +0 -27
- data/test/elements/test_systemwide.rb +0 -38
- data/test/test_accessibility.rb +0 -127
- data/test/test_blankness.rb +0 -26
- data/test/test_core.rb +0 -448
- data/test/test_element.rb +0 -939
- data/test/test_enumerators.rb +0 -81
- data/test/test_inspector.rb +0 -130
- data/test/test_language.rb +0 -157
- data/test/test_macruby_extensions.rb +0 -303
- data/test/test_mouse.rb +0 -5
- data/test/test_search_semantics.rb +0 -143
data/lib/ax/element.rb
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
require 'ax_elements/vendor/inflector'
|
|
4
|
+
require 'accessibility/enumerators'
|
|
5
|
+
require 'accessibility/qualifier'
|
|
6
|
+
require 'accessibility/errors'
|
|
7
|
+
require 'accessibility/pp_inspector'
|
|
8
|
+
require 'accessibility/factory'
|
|
9
|
+
require 'accessibility/core'
|
|
10
|
+
|
|
11
|
+
##
|
|
12
|
+
# @abstract
|
|
13
|
+
#
|
|
14
|
+
# The abstract base class for all accessibility objects. This class
|
|
15
|
+
# provides generic functionality that all accessibility objects require.
|
|
16
|
+
class AX::Element
|
|
17
|
+
include Accessibility::PPInspector
|
|
18
|
+
include Accessibility::Factory
|
|
19
|
+
|
|
20
|
+
# @param [AXUIElementRef]
|
|
21
|
+
def initialize ref
|
|
22
|
+
@ref = ref
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# @group Attributes
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# Cache of available attributes.
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
#
|
|
33
|
+
# window.attributes # => [:size, :position, :title, ...]
|
|
34
|
+
#
|
|
35
|
+
# @return [Array<Symbol>]
|
|
36
|
+
def attributes
|
|
37
|
+
@attributes ||= TRANSLATOR.rubyize @ref.attributes
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
# @todo Consider returning `nil` if the element does not have
|
|
42
|
+
# the given attribute.
|
|
43
|
+
#
|
|
44
|
+
# Get the value of an attribute.
|
|
45
|
+
#
|
|
46
|
+
# @example
|
|
47
|
+
#
|
|
48
|
+
# element.attribute :position # => #<CGPoint x=123.0 y=456.0>
|
|
49
|
+
#
|
|
50
|
+
# @param [#to_sym]
|
|
51
|
+
def attribute name
|
|
52
|
+
if rattr = lookup(name, @ref.attributes)
|
|
53
|
+
process @ref.attribute(rattr)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Needed to override inherited `NSObject#description`. If you want a
|
|
59
|
+
# description of the object then you should use {#inspect} instead.
|
|
60
|
+
def description
|
|
61
|
+
attribute :description
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
##
|
|
65
|
+
# Return the `#size` of an attribute. This only works for attributes
|
|
66
|
+
# that are a collection. This exists because it is _much_ more
|
|
67
|
+
# efficient to find out how many `children` exist using this API
|
|
68
|
+
# instead of getting the children array and asking for the size.
|
|
69
|
+
#
|
|
70
|
+
# @example
|
|
71
|
+
#
|
|
72
|
+
# table.size_of :rows # => 111
|
|
73
|
+
# window.size_of :children # => 16
|
|
74
|
+
#
|
|
75
|
+
# @param [#to_sym]
|
|
76
|
+
# @return [Number]
|
|
77
|
+
def size_of attr
|
|
78
|
+
if rattr = lookup(attr, @ref.attributes)
|
|
79
|
+
@ref.size_of rattr
|
|
80
|
+
else
|
|
81
|
+
0
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
# Get the process identifier for the application that the element
|
|
87
|
+
# belongs to.
|
|
88
|
+
#
|
|
89
|
+
# @example
|
|
90
|
+
#
|
|
91
|
+
# element.pid # => 12345
|
|
92
|
+
#
|
|
93
|
+
# @return [Fixnum]
|
|
94
|
+
def pid
|
|
95
|
+
@ref.pid
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
##
|
|
99
|
+
# Check whether or not an attribute is writable.
|
|
100
|
+
#
|
|
101
|
+
# @example
|
|
102
|
+
#
|
|
103
|
+
# element.writable? :size # => true
|
|
104
|
+
# element.writable? :value # => false
|
|
105
|
+
#
|
|
106
|
+
# @param [#to_sym]
|
|
107
|
+
def writable? attr
|
|
108
|
+
if rattr = lookup(attr, @ref.attributes)
|
|
109
|
+
@ref.writable? rattr
|
|
110
|
+
else
|
|
111
|
+
false
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
##
|
|
116
|
+
# Set a writable attribute on the element to the given value.
|
|
117
|
+
#
|
|
118
|
+
# @example
|
|
119
|
+
#
|
|
120
|
+
# element.set :value, 'Hello, world!'
|
|
121
|
+
# element.set :size, [100, 200].to_size
|
|
122
|
+
#
|
|
123
|
+
# @param [#to_sym]
|
|
124
|
+
# @return the value that you were setting is returned
|
|
125
|
+
def set attr, value
|
|
126
|
+
unless writable? attr
|
|
127
|
+
raise NoMethodError, "#{attr} is read-only for #{inspect}"
|
|
128
|
+
end
|
|
129
|
+
value = value.relative_to(@ref.value.size) if value.kind_of? Range
|
|
130
|
+
rattr = lookup(attr, @ref.attributes)
|
|
131
|
+
@ref.set rattr, value
|
|
132
|
+
value
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# @group Parameterized Attributes
|
|
137
|
+
|
|
138
|
+
##
|
|
139
|
+
# List of available parameterized attributes. Most elements have no
|
|
140
|
+
# parameterized attributes, but the ones that do have many.
|
|
141
|
+
#
|
|
142
|
+
# @example
|
|
143
|
+
#
|
|
144
|
+
# window.parameterized_attributes # => []
|
|
145
|
+
# text_field.parameterized_attributes # => [:string_for_range, :attributed_string, ...]
|
|
146
|
+
#
|
|
147
|
+
# @return [Array<Symbol>]
|
|
148
|
+
def parameterized_attributes
|
|
149
|
+
TRANSLATOR.rubyize @ref.parameterized_attributes
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
##
|
|
153
|
+
# Get the value for a parameterized attribute.
|
|
154
|
+
#
|
|
155
|
+
# @example
|
|
156
|
+
#
|
|
157
|
+
# text_field.attribute :string_for_range, for_param: (2..8).relative_to(10)
|
|
158
|
+
#
|
|
159
|
+
# @param [#to_sym]
|
|
160
|
+
def attribute attr, for_parameter: param
|
|
161
|
+
if rattr = lookup(attr, @ref.parameterized_attributes)
|
|
162
|
+
param = param.relative_to(@ref.value.size) if value.kind_of? Range
|
|
163
|
+
process @ref.attribute(rattr, for_parameter: param)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# @group Actions
|
|
169
|
+
|
|
170
|
+
##
|
|
171
|
+
# List of available actions.
|
|
172
|
+
#
|
|
173
|
+
# @example
|
|
174
|
+
#
|
|
175
|
+
# toolbar.actions # => []
|
|
176
|
+
# button.actions # => [:press]
|
|
177
|
+
# menu.actions # => [:open, :cancel]
|
|
178
|
+
#
|
|
179
|
+
# @return [Array<Symbol>]
|
|
180
|
+
def actions
|
|
181
|
+
TRANSLATOR.rubyize @ref.actions
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
##
|
|
185
|
+
# Tell an object to trigger an action.
|
|
186
|
+
#
|
|
187
|
+
# For instance, you can tell a button to call the same method that
|
|
188
|
+
# would be called when pressing a button, except that the mouse will
|
|
189
|
+
# not move over to the button to press it, nor will the keyboard be
|
|
190
|
+
# used.
|
|
191
|
+
#
|
|
192
|
+
# @example
|
|
193
|
+
#
|
|
194
|
+
# button.perform :press # => true
|
|
195
|
+
#
|
|
196
|
+
# @param [#to_sym]
|
|
197
|
+
# @return [Boolean] true if successful
|
|
198
|
+
def perform action
|
|
199
|
+
if raction = lookup(action, @ref.actions)
|
|
200
|
+
@ref.perform raction
|
|
201
|
+
else
|
|
202
|
+
false
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# @group Search
|
|
208
|
+
|
|
209
|
+
##
|
|
210
|
+
# Perform a breadth first search through the view hierarchy rooted at
|
|
211
|
+
# the current element.
|
|
212
|
+
#
|
|
213
|
+
# See the {file:docs/Searching.markdown Searching} tutorial for the
|
|
214
|
+
# details on searching.
|
|
215
|
+
#
|
|
216
|
+
# @example Find the dock icon for the Finder app
|
|
217
|
+
#
|
|
218
|
+
# AX::DOCK.search( :application_dock_item, title:'Finder' )
|
|
219
|
+
#
|
|
220
|
+
# @param [#to_s]
|
|
221
|
+
# @param [Hash{Symbol=>Object}]
|
|
222
|
+
# @return [AX::Element,nil,Array<AX::Element>,Array<>]
|
|
223
|
+
def search kind, filters = {}, &block
|
|
224
|
+
kind = kind.to_s
|
|
225
|
+
qualifier = Accessibility::Qualifier.new(kind, filters, &block)
|
|
226
|
+
tree = Accessibility::Enumerators::BreadthFirst.new(self)
|
|
227
|
+
|
|
228
|
+
if TRANSLATOR.singularize(kind) == kind
|
|
229
|
+
tree.find { |element| qualifier.qualifies? element }
|
|
230
|
+
else
|
|
231
|
+
tree.find_all { |element| qualifier.qualifies? element }
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
##
|
|
236
|
+
# Search for an ancestor of the current elemenet.
|
|
237
|
+
#
|
|
238
|
+
# As the opposite of {#search}, this also takes filters, and can
|
|
239
|
+
# be used to find a specific ancestor for the current element.
|
|
240
|
+
#
|
|
241
|
+
# @example
|
|
242
|
+
#
|
|
243
|
+
# button.ancestor :window # => #<AX::StandardWindow>
|
|
244
|
+
# row.ancestor :scroll_area # => #<AX::ScrollArea>
|
|
245
|
+
#
|
|
246
|
+
# @param [#to_s]
|
|
247
|
+
# @param [Hash{Symbol=>Object}]
|
|
248
|
+
# @return [AX::Element]
|
|
249
|
+
def ancestor kind, filters = {}, &block
|
|
250
|
+
qualifier = Accessibility::Qualifier.new(kind, filters, &block)
|
|
251
|
+
element = attribute :parent
|
|
252
|
+
until qualifier.qualifies? element
|
|
253
|
+
element = element.attribute :parent
|
|
254
|
+
end
|
|
255
|
+
element
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
##
|
|
259
|
+
# We use {#method_missing} to dynamically handle requests to lookup
|
|
260
|
+
# attributes or search for elements in the view hierarchy. An attribute
|
|
261
|
+
# lookup is always tried first, followed by a parameterized attribute
|
|
262
|
+
# lookup, and then finally a search.
|
|
263
|
+
#
|
|
264
|
+
# Failing all lookups, this method calls `super`.
|
|
265
|
+
#
|
|
266
|
+
# @example
|
|
267
|
+
#
|
|
268
|
+
# mail = AX::Application.application_with_bundle_identifier 'com.apple.mail'
|
|
269
|
+
#
|
|
270
|
+
# # attribute lookup
|
|
271
|
+
# window = mail.focused_window
|
|
272
|
+
# # is equivalent to
|
|
273
|
+
# window = mail.attribute :focused_window
|
|
274
|
+
#
|
|
275
|
+
# # parameterized attribute lookup
|
|
276
|
+
# window.title_ui_element.string_for_range (1..10).relative_to(100)
|
|
277
|
+
# # is equivalent to
|
|
278
|
+
# title = window.attribute :title_ui_element
|
|
279
|
+
# title.param_attribute :string_for_range, for_param: (1..10).relative_to(100)
|
|
280
|
+
#
|
|
281
|
+
# # simple single element search
|
|
282
|
+
# window.button # => You want the first Button that is found
|
|
283
|
+
# # is equivalent to
|
|
284
|
+
# window.search :button, {}
|
|
285
|
+
#
|
|
286
|
+
# # simple multi-element search
|
|
287
|
+
# window.buttons # => You want all the Button objects found
|
|
288
|
+
# # is equivalent to
|
|
289
|
+
# window.search :buttons, {}
|
|
290
|
+
#
|
|
291
|
+
# # filters for a single element search
|
|
292
|
+
# window.button(title: 'Log In') # => First Button with a title of 'Log In'
|
|
293
|
+
# # is equivalent to
|
|
294
|
+
# window.search :button, title: 'Log In'
|
|
295
|
+
#
|
|
296
|
+
# # attribute and element search failure
|
|
297
|
+
# window.application # => SearchFailure is raised
|
|
298
|
+
#
|
|
299
|
+
def method_missing method, *args, &block
|
|
300
|
+
if method[-1] == EQUALS
|
|
301
|
+
return set(method.chomp(EQUALS), args.first)
|
|
302
|
+
|
|
303
|
+
elsif attr = lookup(method, @ref.attributes)
|
|
304
|
+
return process @ref.attribute(attr)
|
|
305
|
+
|
|
306
|
+
elsif attr = lookup(method, @ref.parameterized_attributes)
|
|
307
|
+
return process @ref.attribute(attr, for_parameter: args.first)
|
|
308
|
+
|
|
309
|
+
elsif @ref.attributes.include? KAXChildrenAttribute
|
|
310
|
+
result = search method, *args, &block
|
|
311
|
+
return result unless result.blank?
|
|
312
|
+
raise Accessibility::SearchFailure.new(self, method, args.first)
|
|
313
|
+
|
|
314
|
+
else
|
|
315
|
+
super
|
|
316
|
+
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# @group Notifications
|
|
322
|
+
|
|
323
|
+
def notifs
|
|
324
|
+
@notifs ||= {}
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
##
|
|
328
|
+
# Register to receive notification of the given event being completed
|
|
329
|
+
# by the given element.
|
|
330
|
+
#
|
|
331
|
+
# {file:docs/Notifications.markdown Notifications} are a way to put
|
|
332
|
+
# non-polling delays into your scripts.
|
|
333
|
+
#
|
|
334
|
+
# Use this method to register to be notified of the specified event in
|
|
335
|
+
# an application.
|
|
336
|
+
#
|
|
337
|
+
# The block is optional. The block will be given the sender of the
|
|
338
|
+
# notification, which will almost always be `self`, and also the name
|
|
339
|
+
# of the notification being received. The block should return a
|
|
340
|
+
# boolean value that decides if the notification received is the
|
|
341
|
+
# expected one.
|
|
342
|
+
#
|
|
343
|
+
# @example
|
|
344
|
+
#
|
|
345
|
+
# on_notification(:window_created) { |sender|
|
|
346
|
+
# puts "#{sender.inspect} sent the ':window_created' notification"
|
|
347
|
+
# true
|
|
348
|
+
# }
|
|
349
|
+
#
|
|
350
|
+
# @param [#to_s] notif the name of the notification
|
|
351
|
+
# @yield Validate the notification; the block should return truthy if
|
|
352
|
+
# the notification received is the expected one and the script can
|
|
353
|
+
# stop waiting, otherwise should return falsy.
|
|
354
|
+
# @yieldparam [String] notif the name of the notification
|
|
355
|
+
# @yieldparam [AXUIElementRef] element the element that sent the notification
|
|
356
|
+
# @yieldreturn [Boolean]
|
|
357
|
+
# @return [Array(Observer, String, CFRunLoopSource)]
|
|
358
|
+
def on_notification name, &block
|
|
359
|
+
notif = TRANSLATOR.guess_notification_for name
|
|
360
|
+
observer = @ref.observer ¬if_callback_for(&block)
|
|
361
|
+
source = @ref.run_loop_source_for observer
|
|
362
|
+
@ref.register observer, to_receive: notif
|
|
363
|
+
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, KCFRunLoopDefaultMode)
|
|
364
|
+
notifs[name] = [observer, notif, source]
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def unregister_notification name
|
|
368
|
+
unless notifs.has_key? name
|
|
369
|
+
raise ArgumentError, "You have no registrations for #{name}"
|
|
370
|
+
end
|
|
371
|
+
_unregister_notification *notifs.delete(name)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
##
|
|
375
|
+
# Cancel _all_ notification registrations for this object. Simple and
|
|
376
|
+
# clean, but a blunt tool at best. This will have to do for the time
|
|
377
|
+
# being...
|
|
378
|
+
#
|
|
379
|
+
# @return [nil]
|
|
380
|
+
def unregister_all
|
|
381
|
+
notifs.keys.each do |notif|
|
|
382
|
+
unregister_notification notif
|
|
383
|
+
end
|
|
384
|
+
nil
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# @endgroup
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
##
|
|
391
|
+
# Overriden to produce cleaner output.
|
|
392
|
+
#
|
|
393
|
+
# @return [String]
|
|
394
|
+
def inspect
|
|
395
|
+
msg = "#<#{self.class}" << pp_identifier
|
|
396
|
+
attrs = @ref.attributes
|
|
397
|
+
msg << pp_position if attrs.include? KAXPositionAttribute
|
|
398
|
+
msg << pp_children if attrs.include? KAXChildrenAttribute
|
|
399
|
+
msg << pp_checkbox(:enabled) if attrs.include? KAXEnabledAttribute
|
|
400
|
+
msg << pp_checkbox(:focused) if attrs.include? KAXFocusedAttribute
|
|
401
|
+
msg << '>'
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
##
|
|
405
|
+
# @note Since `#inspect` is often overridden by subclasses, this cannot
|
|
406
|
+
# be an alias.
|
|
407
|
+
#
|
|
408
|
+
# An "alias" for {#inspect}.
|
|
409
|
+
#
|
|
410
|
+
# @return [String]
|
|
411
|
+
def to_s
|
|
412
|
+
inspect
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
##
|
|
416
|
+
# Overriden to respond properly with regards to dynamic attribute
|
|
417
|
+
# lookups, but will return false for potential implicit searches.
|
|
418
|
+
def respond_to? name
|
|
419
|
+
lookup(name.chomp(EQUALS), @ref.attributes) ||
|
|
420
|
+
lookup(name, @ref.parameterized_attributes) ||
|
|
421
|
+
super
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
##
|
|
425
|
+
# Get the center point of the element.
|
|
426
|
+
#
|
|
427
|
+
# @return [CGPoint]
|
|
428
|
+
def to_point
|
|
429
|
+
size = attribute :size
|
|
430
|
+
point = attribute :position
|
|
431
|
+
point.x += size.width / 2
|
|
432
|
+
point.y += size.height / 2
|
|
433
|
+
point
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
##
|
|
437
|
+
# Get the bounding rectangle for the element.
|
|
438
|
+
#
|
|
439
|
+
# @return [CGRect]
|
|
440
|
+
def bounds
|
|
441
|
+
point = attribute :position
|
|
442
|
+
size = attribute :size
|
|
443
|
+
CGRectMake(*point, *size)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
##
|
|
447
|
+
# Get the application object for the element.
|
|
448
|
+
#
|
|
449
|
+
# @return [AX::Application]
|
|
450
|
+
def application
|
|
451
|
+
process @ref.application
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
##
|
|
455
|
+
# Concept borrowed from `Active Support`. It is used during implicit
|
|
456
|
+
# searches to determine if searches yielded responses.
|
|
457
|
+
def blank?
|
|
458
|
+
false
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
##
|
|
462
|
+
# @todo Need to add '?' to predicate methods, but how?
|
|
463
|
+
#
|
|
464
|
+
# Like {#respond_to?}, this is overriden to include attribute methods.
|
|
465
|
+
def methods include_super = true, include_objc_super = false
|
|
466
|
+
super.concat(attributes).concat(parameterized_attributes)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
##
|
|
470
|
+
# Overridden so that equality testing would work. A hack, but the only
|
|
471
|
+
# sane way I can think of to test for equivalency.
|
|
472
|
+
def == other
|
|
473
|
+
@ref == other.instance_variable_get(:@ref)
|
|
474
|
+
end
|
|
475
|
+
alias_method :eql?, :==
|
|
476
|
+
alias_method :equal?, :==
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
private
|
|
480
|
+
|
|
481
|
+
##
|
|
482
|
+
# @private
|
|
483
|
+
#
|
|
484
|
+
# Performance hack.
|
|
485
|
+
#
|
|
486
|
+
# @return [String]
|
|
487
|
+
EQUALS = '='
|
|
488
|
+
|
|
489
|
+
def lookup key, values
|
|
490
|
+
value = TRANSLATOR.lookup key, values
|
|
491
|
+
return value if values.include? value
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def notif_callback_for
|
|
495
|
+
# we are ignoring the context pointer since this is OO
|
|
496
|
+
Proc.new do |observer, sender, notif, _|
|
|
497
|
+
break unless yield(process sender) if block_given?
|
|
498
|
+
_unregister_notification observer, notif, run_loop_source_for(observer)
|
|
499
|
+
CFRunLoopStop(CFRunLoopGetCurrent())
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
##
|
|
504
|
+
# @todo What are the implications of removing the run loop source?
|
|
505
|
+
# Taking it out would clobber other notifications that are using
|
|
506
|
+
# the same source, so we would have to check if we can remove it.
|
|
507
|
+
#
|
|
508
|
+
def _unregister_notification observer, notif, source
|
|
509
|
+
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, KCFRunLoopDefaultMode)
|
|
510
|
+
@ref.unregister observer, from_receiving: notif
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
# Extensions so checking #blank? on search result "just works".
|
|
517
|
+
class NSArray; alias_method :blank?, :empty? end
|
|
518
|
+
class NilClass; def blank?; true end end
|
data/lib/ax/row.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require 'ax/element'
|
|
2
|
+
require 'accessibility/qualifier'
|
|
3
|
+
|
|
4
|
+
##
|
|
5
|
+
# UI Element for the row in a table, outline, etc.
|
|
6
|
+
class AX::Row < AX::Element
|
|
7
|
+
|
|
8
|
+
##
|
|
9
|
+
# Retrieve the child in a row that corresponds to a specific column.
|
|
10
|
+
# You must pass filters here in the same way that you would for a
|
|
11
|
+
# search.
|
|
12
|
+
#
|
|
13
|
+
# This is useful for tables where it is difficult to identify which
|
|
14
|
+
# row item is the one you want based on the row items themselves.
|
|
15
|
+
# Often times the columns in the table will have identifying attributes,
|
|
16
|
+
# such as a header, and so you can use this method to figure out what
|
|
17
|
+
# column your row item is in and then the method will return the row
|
|
18
|
+
# item you wanted.
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
#
|
|
22
|
+
# rows = table.rows
|
|
23
|
+
# total = rows.inject(0) { |sum, row|
|
|
24
|
+
# sum += row.child_in_column(header: 'Price').value.to_i
|
|
25
|
+
# }
|
|
26
|
+
# puts "The average price is $ #{total / rows.size}"
|
|
27
|
+
#
|
|
28
|
+
# @param [Hash]
|
|
29
|
+
# @return [AX::Element]
|
|
30
|
+
def child_in_column filters, &block
|
|
31
|
+
qualifier = Accessibility::Qualifier.new(:Column, filters, &block)
|
|
32
|
+
column = self.parent.columns.index { |x| qualifier.qualifies? x }
|
|
33
|
+
return self.children.at(column) if column
|
|
34
|
+
raise Accessibility::SearchFailure.new(self.parent, 'column', filters)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require 'ax/element'
|
|
2
|
+
require 'accessibility/string'
|
|
3
|
+
|
|
4
|
+
##
|
|
5
|
+
# Represents the special `SystemWide` accessibility object.
|
|
6
|
+
#
|
|
7
|
+
# Previously, this object was a singleton, but that apparently causes
|
|
8
|
+
# problems with the AXAPIs. So you should always create a new instance
|
|
9
|
+
# of the system wide object when you need to use it (even though they
|
|
10
|
+
# are all the same thing).
|
|
11
|
+
class AX::SystemWide < AX::Element
|
|
12
|
+
include Accessibility::String
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# Overridden since there is only one way to get the element ref.
|
|
16
|
+
def initialize
|
|
17
|
+
super AXUIElementCreateSystemWide()
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# @note With the `SystemWide` class, using {#type_string} will send the
|
|
22
|
+
# events to which ever app has focus.
|
|
23
|
+
#
|
|
24
|
+
# Generate keyboard events by simulating keyboard input.
|
|
25
|
+
#
|
|
26
|
+
# See the {file:docs/KeyboardEvents.markdown Keyboard} documentation for
|
|
27
|
+
# more information on how to format strings.
|
|
28
|
+
#
|
|
29
|
+
# @return [Boolean]
|
|
30
|
+
def type_string string
|
|
31
|
+
@ref.post keyboard_events_for string
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @todo doc and cleanup
|
|
36
|
+
def keydown modifier
|
|
37
|
+
@ref.post [[EventGenerator::CUSTOM[modifier], true]]
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @todo doc and cleanup
|
|
42
|
+
def keyup modifier
|
|
43
|
+
@ref.post [[EventGenerator::CUSTOM[modifier], false]]
|
|
44
|
+
true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
##
|
|
48
|
+
# The system wide object cannot be used to perform searches. This method
|
|
49
|
+
# is just an override to avoid a difficult to understand error messages.
|
|
50
|
+
def search *args
|
|
51
|
+
raise NoMethodError, 'AX::SystemWide cannot search'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# Raises an `NoMethodError` instead of (possibly) silently failing to
|
|
56
|
+
# register for a notification.
|
|
57
|
+
#
|
|
58
|
+
# @raise [NoMethodError]
|
|
59
|
+
def on_notification *args
|
|
60
|
+
raise NoMethodError, 'AX::SystemWide cannot register for notifications'
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
##
|
|
64
|
+
# Find the element in at the given point for the topmost appilcation
|
|
65
|
+
# window.
|
|
66
|
+
#
|
|
67
|
+
# `nil` will be returned if there was nothing at that point.
|
|
68
|
+
#
|
|
69
|
+
# @param [#to_point]
|
|
70
|
+
# @return [AX::Element,nil]
|
|
71
|
+
def element_at point
|
|
72
|
+
process @ref.element_at point
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
##
|
|
76
|
+
# Set the global messaging timeout. Searching through another interface
|
|
77
|
+
# and looking up attributes incurs a lot of IPC calls and sometimes an
|
|
78
|
+
# app is slow to respond.
|
|
79
|
+
#
|
|
80
|
+
# @param [Number]
|
|
81
|
+
# @return [Number]
|
|
82
|
+
def set_global_timeout seconds
|
|
83
|
+
@ref.set_timeout_to seconds
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'awesome_print'
|
|
2
|
+
|
|
3
|
+
module AwesomePrint::AXElements
|
|
4
|
+
|
|
5
|
+
def self.included base
|
|
6
|
+
base.send :alias_method, :cast_without_ax_elements, :cast
|
|
7
|
+
base.send :alias_method, :cast, :cast_with_ax_elements
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def cast_with_ax_elements object, type
|
|
11
|
+
cast = cast_without_ax_elements object, type
|
|
12
|
+
cast = :ax_element if object.kind_of? ::AX::Element
|
|
13
|
+
cast
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def awesome_ax_element object
|
|
20
|
+
object.inspect
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
AwesomePrint::Formatter.send :include, AwesomePrint::AXElements
|