AXElements 0.6.0beta2 → 0.7.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|