AXElements 0.6.0beta1
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +20 -0
- data/LICENSE.txt +25 -0
- data/README.markdown +150 -0
- data/Rakefile +109 -0
- data/docs/AccessibilityTips.markdown +119 -0
- data/docs/Acting.markdown +340 -0
- data/docs/Debugging.markdown +326 -0
- data/docs/Inspecting.markdown +255 -0
- data/docs/KeyboardEvents.markdown +57 -0
- data/docs/NewBehaviour.markdown +151 -0
- data/docs/Notifications.markdown +271 -0
- data/docs/Searching.markdown +250 -0
- data/docs/TestingExtensions.markdown +52 -0
- data/docs/images/AX.png +0 -0
- data/docs/images/all_the_buttons.jpg +0 -0
- data/docs/images/ui_hierarchy.dot +34 -0
- data/docs/images/ui_hierarchy.png +0 -0
- data/ext/key_coder/extconf.rb +6 -0
- data/ext/key_coder/key_coder.m +77 -0
- data/lib/ax_elements/accessibility/enumerators.rb +104 -0
- data/lib/ax_elements/accessibility/language.rb +347 -0
- data/lib/ax_elements/accessibility/qualifier.rb +73 -0
- data/lib/ax_elements/accessibility.rb +164 -0
- data/lib/ax_elements/core.rb +541 -0
- data/lib/ax_elements/element.rb +593 -0
- data/lib/ax_elements/elements/application.rb +88 -0
- data/lib/ax_elements/elements/button.rb +18 -0
- data/lib/ax_elements/elements/radio_button.rb +18 -0
- data/lib/ax_elements/elements/row.rb +30 -0
- data/lib/ax_elements/elements/static_text.rb +17 -0
- data/lib/ax_elements/elements/systemwide.rb +46 -0
- data/lib/ax_elements/inspector.rb +116 -0
- data/lib/ax_elements/macruby_extensions.rb +255 -0
- data/lib/ax_elements/notification.rb +37 -0
- data/lib/ax_elements/version.rb +9 -0
- data/lib/ax_elements.rb +30 -0
- data/lib/minitest/ax_elements.rb +19 -0
- data/lib/mouse.rb +185 -0
- data/lib/rspec/expectations/ax_elements.rb +15 -0
- data/test/elements/test_application.rb +72 -0
- data/test/elements/test_row.rb +27 -0
- data/test/elements/test_systemwide.rb +38 -0
- data/test/helper.rb +119 -0
- data/test/test_accessibility.rb +127 -0
- data/test/test_blankness.rb +26 -0
- data/test/test_core.rb +448 -0
- data/test/test_element.rb +939 -0
- data/test/test_enumerators.rb +81 -0
- data/test/test_inspector.rb +121 -0
- data/test/test_language.rb +157 -0
- data/test/test_macruby_extensions.rb +303 -0
- data/test/test_mouse.rb +5 -0
- data/test/test_search_semantics.rb +143 -0
- metadata +219 -0
@@ -0,0 +1,593 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'active_support/inflector'
|
4
|
+
require 'ax_elements/inspector'
|
5
|
+
require 'ax_elements/accessibility'
|
6
|
+
|
7
|
+
##
|
8
|
+
# @abstract
|
9
|
+
#
|
10
|
+
# The abstract base class for all accessibility objects.
|
11
|
+
class AX::Element
|
12
|
+
include Accessibility::PPInspector
|
13
|
+
|
14
|
+
##
|
15
|
+
# Raised when a lookup fails
|
16
|
+
class LookupFailure < ArgumentError
|
17
|
+
def initialize element, name
|
18
|
+
super "#{name} was not found for #{element.inspect}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Raised when trying to set an attribute that cannot be written
|
24
|
+
class ReadOnlyAttribute < NoMethodError
|
25
|
+
def initialize element, name
|
26
|
+
super "#{name} is a read only attribute for #{element.inspect}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Raised when an implicit search fails
|
32
|
+
class SearchFailure < NoMethodError
|
33
|
+
def initialize searcher, searchee, filters
|
34
|
+
path = Accessibility.path(searcher).map! { |x| x.inspect }
|
35
|
+
pp_filters = (filters || {}).map do |key, value|
|
36
|
+
"#{key}: #{value.inspect}"
|
37
|
+
end.join(', ')
|
38
|
+
msg = "Could not find `#{searchee}"
|
39
|
+
msg << "(#{pp_filters})" unless pp_filters.empty?
|
40
|
+
msg << "` as a child of #{searcher.class}"
|
41
|
+
msg << "\nElement Path:\n\t" << path.join("\n\t")
|
42
|
+
super msg
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @param [AXUIElementRef]
|
47
|
+
# @param [Array<String>]
|
48
|
+
def initialize ref, attrs
|
49
|
+
@ref = ref
|
50
|
+
@attributes = attrs
|
51
|
+
end
|
52
|
+
|
53
|
+
# @group Attributes
|
54
|
+
|
55
|
+
##
|
56
|
+
# Cache of available attributes.
|
57
|
+
#
|
58
|
+
# @return [Array<String>]
|
59
|
+
attr_reader :attributes
|
60
|
+
|
61
|
+
##
|
62
|
+
# Get the value of an attribute.
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
#
|
66
|
+
# element.attribute :position # => "#<CGPoint x=123.0 y=456.0>"
|
67
|
+
#
|
68
|
+
# @param [Symbol]
|
69
|
+
def attribute attr
|
70
|
+
real_attr = attribute_for attr
|
71
|
+
raise LookupFailure.new(self, attr) unless real_attr
|
72
|
+
self.class.attribute_for @ref, real_attr
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Needed to override inherited `NSObject#description`. If you want a
|
77
|
+
# description of the object use {#inspect} instead.
|
78
|
+
def description
|
79
|
+
attribute :description
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# You can use this method to find out the `#size` of an array that is
|
84
|
+
# an attribute of the element. This exists because it is _much_ more
|
85
|
+
# efficient to find out how many `children` exist using this API instead
|
86
|
+
# of getting the children array and asking for the size.
|
87
|
+
#
|
88
|
+
# @example
|
89
|
+
#
|
90
|
+
# button.size_of :children # => 0
|
91
|
+
# window.size_of :children # => 16
|
92
|
+
#
|
93
|
+
# @param [Symbol]
|
94
|
+
# @return [Number]
|
95
|
+
def size_of attr
|
96
|
+
real_attr = attribute_for attr
|
97
|
+
raise LookupFailure.new(self, attr) unless real_attr
|
98
|
+
AX.attr_count_of_element @ref, real_attr
|
99
|
+
end
|
100
|
+
|
101
|
+
##
|
102
|
+
# Get the process identifier for the application that the element
|
103
|
+
# belongs to.
|
104
|
+
#
|
105
|
+
# @return [Fixnum]
|
106
|
+
def pid
|
107
|
+
@pid ||= AX.pid_of_element @ref
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# Check whether or not an attribute is writable.
|
112
|
+
#
|
113
|
+
# @param [Symbol] attr
|
114
|
+
def attribute_writable? attr
|
115
|
+
real_attribute = attribute_for attr
|
116
|
+
raise LookupFailure.new(self, attr) unless real_attribute
|
117
|
+
AX.attr_of_element_writable? @ref, real_attribute
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# @note Due to the way thot `Boxed` objects are taken care of, you
|
122
|
+
# cannot pass tuples in place of the `Boxed` object. This may
|
123
|
+
# change in the future.
|
124
|
+
#
|
125
|
+
# Set a writable attribute on the element to the given value.
|
126
|
+
#
|
127
|
+
# @param [String] attr an attribute constant
|
128
|
+
# @return the value that you were setting is returned
|
129
|
+
def set_attribute attr, value
|
130
|
+
raise ReadOnlyAttribute.new(self, attr) unless attribute_writable? attr
|
131
|
+
real_attribute = attribute_for attr
|
132
|
+
value = value.to_axvalue if value.kind_of? Boxed
|
133
|
+
AX.set_attr_of_element @ref, real_attribute, value
|
134
|
+
value
|
135
|
+
end
|
136
|
+
|
137
|
+
# @group Parameterized Attributes
|
138
|
+
|
139
|
+
##
|
140
|
+
# List of available parameterized attributes
|
141
|
+
#
|
142
|
+
# @return [Array<String>]
|
143
|
+
def param_attributes
|
144
|
+
@param_attributes ||= AX.param_attrs_of_element @ref
|
145
|
+
end
|
146
|
+
|
147
|
+
##
|
148
|
+
# @note Due to the way thot `Boxed` objects are taken care of, you
|
149
|
+
# cannot pass tuples in place of the `Boxed` object. This may
|
150
|
+
# change in the future.
|
151
|
+
#
|
152
|
+
# Get the value for a parameterized attribute.
|
153
|
+
#
|
154
|
+
# @param [Symbol]
|
155
|
+
def param_attribute attr, param
|
156
|
+
real_attr = param_attribute_for attr
|
157
|
+
raise LookupFailure.new(self, attr) unless real_attr
|
158
|
+
param = param.to_axvalue if param.kind_of? Boxed
|
159
|
+
self.class.param_attribute_for @ref, real_attr, param
|
160
|
+
end
|
161
|
+
|
162
|
+
# @group Actions
|
163
|
+
|
164
|
+
##
|
165
|
+
# List of available actions.
|
166
|
+
#
|
167
|
+
# @return [Array<String>]
|
168
|
+
def actions
|
169
|
+
@actions ||= AX.actions_of_element @ref
|
170
|
+
end
|
171
|
+
|
172
|
+
##
|
173
|
+
# @note Ideally this method would return a reference to `self`, but
|
174
|
+
# since intrinsically causes state change in the app being
|
175
|
+
# manipulate, the reference to `self` may no longer be valid.
|
176
|
+
# An example of this would be pressing the close button on a
|
177
|
+
# window.
|
178
|
+
#
|
179
|
+
# Tell an object to trigger an action without actually performing
|
180
|
+
# the action.
|
181
|
+
#
|
182
|
+
# For instance, you can tell a button to call the same method that
|
183
|
+
# would be called when pressing a button, except that the mouse will
|
184
|
+
# not move over to the button to press it, nor will the keyboard be
|
185
|
+
# used.
|
186
|
+
#
|
187
|
+
# @example
|
188
|
+
#
|
189
|
+
# element.perform_action :press # => true
|
190
|
+
#
|
191
|
+
# @param [String] name an action constant
|
192
|
+
# @return [Boolean] true if successful
|
193
|
+
def perform_action name
|
194
|
+
real_action = action_for name
|
195
|
+
raise LookupFailure.new(self, name) unless real_action
|
196
|
+
AX.action_of_element @ref, real_action
|
197
|
+
end
|
198
|
+
|
199
|
+
# @group Search
|
200
|
+
|
201
|
+
##
|
202
|
+
# Perform a breadth first search through the view hierarchy rooted at
|
203
|
+
# the current element.
|
204
|
+
#
|
205
|
+
# See the {file:docs/Searching.markdown Searching} tutorial for the
|
206
|
+
# details on searching.
|
207
|
+
#
|
208
|
+
# @example Find the dock icon for the Finder app
|
209
|
+
#
|
210
|
+
# AX::DOCK.search( :application_dock_item, title:'Finder' )
|
211
|
+
#
|
212
|
+
# @param [#to_s] kind
|
213
|
+
# @param [Hash{Symbol=>Object}] filters
|
214
|
+
# @return [AX::Element,nil,Array<AX::Element>,Array<>]
|
215
|
+
def search kind, filters = {}
|
216
|
+
kind = kind.camelize
|
217
|
+
klass = kind.singularize
|
218
|
+
search = klass == kind ? :find : :find_all
|
219
|
+
qualifier = Accessibility::Qualifier.new(klass, filters)
|
220
|
+
tree = Accessibility::BFEnumerator.new(self)
|
221
|
+
|
222
|
+
tree.send(search) { |element| qualifier.qualifies? element }
|
223
|
+
end
|
224
|
+
|
225
|
+
##
|
226
|
+
# We use {#method_missing} to dynamically handle requests to lookup
|
227
|
+
# attributes or search for elements in the view hierarchy. An attribute
|
228
|
+
# lookup is always tried first, followed by a parameterized attribute
|
229
|
+
# lookup, and then finally a search.
|
230
|
+
#
|
231
|
+
# Failing both lookups, this method calls `super`.
|
232
|
+
#
|
233
|
+
# @example Attribute lookup of an element
|
234
|
+
#
|
235
|
+
# mail = AX::Application.application_with_bundle_identifier 'com.apple.mail'
|
236
|
+
# window = mail.focused_window
|
237
|
+
#
|
238
|
+
# @example Attribute lookup of an element property
|
239
|
+
#
|
240
|
+
# window.title
|
241
|
+
#
|
242
|
+
# @example Simple single element search
|
243
|
+
#
|
244
|
+
# window.button # => You want the first Button that is found
|
245
|
+
#
|
246
|
+
# @example Simple multi-element search
|
247
|
+
#
|
248
|
+
# window.buttons # => You want all the Button objects found
|
249
|
+
#
|
250
|
+
# @example Filters for a single element search
|
251
|
+
#
|
252
|
+
# window.button(title:'Log In') # => First Button with a title of 'Log In'
|
253
|
+
#
|
254
|
+
# @example Contrived multi-element search with filtering
|
255
|
+
#
|
256
|
+
# window.buttons(title:'New Project', enabled:true)
|
257
|
+
#
|
258
|
+
# @example Attribute and element search failure
|
259
|
+
#
|
260
|
+
# window.application # => SearchFailure is raised
|
261
|
+
#
|
262
|
+
# @example Parameterized Attribute lookup
|
263
|
+
#
|
264
|
+
# text = window.title_ui_element
|
265
|
+
# text.string_for_range(CFRange.new(0, 1))
|
266
|
+
#
|
267
|
+
def method_missing method, *args
|
268
|
+
if attr = attribute_for(method)
|
269
|
+
return self.class.attribute_for(@ref, attr)
|
270
|
+
|
271
|
+
elsif attr = param_attribute_for(method)
|
272
|
+
return self.class.param_attribute_for(@ref, attr, args.first)
|
273
|
+
|
274
|
+
elsif attributes.include? KAXChildrenAttribute
|
275
|
+
result = search method, *args
|
276
|
+
return result unless result.blank?
|
277
|
+
raise SearchFailure.new(self, method, args.first)
|
278
|
+
|
279
|
+
else
|
280
|
+
super
|
281
|
+
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# @group Notifications
|
286
|
+
|
287
|
+
##
|
288
|
+
# Register to receive a notification from the object.
|
289
|
+
#
|
290
|
+
# You can optionally pass a block to this method that will be given
|
291
|
+
# an element equivalent to `self` and the name of the notification;
|
292
|
+
# the block should return a boolean value that decides if the
|
293
|
+
# notification received is the expected one.
|
294
|
+
#
|
295
|
+
# Read the {file:docs/Notifications.markdown Notifications tutorial}
|
296
|
+
# for more information.
|
297
|
+
#
|
298
|
+
# @param [String,Symbol]
|
299
|
+
# @param [Float] timeout
|
300
|
+
# @yieldparam [AX::Element] element
|
301
|
+
# @yieldparam [String] notif
|
302
|
+
# @yieldreturn [Boolean]
|
303
|
+
# @return [Array(self,String)] an (element, notification) pair
|
304
|
+
def on_notification notif, &block
|
305
|
+
name = notif_for notif
|
306
|
+
AX.register_for_notif(@ref, name) do |element, notification|
|
307
|
+
element = self.class.process element
|
308
|
+
block ? block.call(element, notification) : true
|
309
|
+
end
|
310
|
+
[self, name]
|
311
|
+
end
|
312
|
+
|
313
|
+
# @endgroup
|
314
|
+
|
315
|
+
##
|
316
|
+
# Overriden to produce cleaner output.
|
317
|
+
#
|
318
|
+
# @return [String]
|
319
|
+
def inspect
|
320
|
+
msg = "#<#{self.class}" << pp_identifier
|
321
|
+
msg << pp_position if attributes.include? KAXPositionAttribute
|
322
|
+
msg << pp_children if attributes.include? KAXChildrenAttribute
|
323
|
+
msg << pp_checkbox(:enabled) if attributes.include? KAXEnabledAttribute
|
324
|
+
msg << pp_checkbox(:focused) if attributes.include? KAXFocusedAttribute
|
325
|
+
msg << '>'
|
326
|
+
end
|
327
|
+
|
328
|
+
##
|
329
|
+
# Overriden to respond properly with regards to the ydnamic attribute
|
330
|
+
# lookups, but will return false for potential implicit searches.
|
331
|
+
def respond_to? name
|
332
|
+
return true if attribute_for name
|
333
|
+
return true if param_attribute_for name
|
334
|
+
return attributes.include? KAXDescriptionAttribute if name == :description
|
335
|
+
return super
|
336
|
+
end
|
337
|
+
|
338
|
+
##
|
339
|
+
# Get the center point of the element.
|
340
|
+
#
|
341
|
+
# @return [CGPoint]
|
342
|
+
def to_point
|
343
|
+
attribute(:position).center(attribute :size)
|
344
|
+
end
|
345
|
+
|
346
|
+
##
|
347
|
+
# Used during implicit search to determine if searches yielded
|
348
|
+
# responses.
|
349
|
+
def blank?
|
350
|
+
false
|
351
|
+
end
|
352
|
+
|
353
|
+
##
|
354
|
+
# @todo Need to add '?' to predicate methods, but how?
|
355
|
+
#
|
356
|
+
# Like {#respond_to?}, this is overriden to include attribute methods.
|
357
|
+
def methods include_super = true, include_objc_super = false
|
358
|
+
names = attributes.map { |x| self.class.strip_prefix(x).underscore.to_sym }
|
359
|
+
names.concat super
|
360
|
+
end
|
361
|
+
|
362
|
+
##
|
363
|
+
# Overridden so that equality testing would work. A hack, but the only
|
364
|
+
# sane way I can think of to test for equivalency.
|
365
|
+
def == other
|
366
|
+
@ref == other.instance_variable_get(:@ref)
|
367
|
+
end
|
368
|
+
alias_method :eql?, :==
|
369
|
+
alias_method :equal?, :==
|
370
|
+
|
371
|
+
# @todo Do we need to override #=== as well?
|
372
|
+
|
373
|
+
|
374
|
+
protected
|
375
|
+
|
376
|
+
##
|
377
|
+
# Try to turn an arbitrary symbol into notification constant, and
|
378
|
+
# then get the value of the constant.
|
379
|
+
#
|
380
|
+
# @param [Symbol,String]
|
381
|
+
# @return [String]
|
382
|
+
def notif_for name
|
383
|
+
name = name.to_s
|
384
|
+
const = "KAX#{name.camelize}Notification"
|
385
|
+
Kernel.const_defined?(const) ? Kernel.const_get(const) : name
|
386
|
+
end
|
387
|
+
|
388
|
+
##
|
389
|
+
# Find the constant value for the given symbol. If nothing is found
|
390
|
+
# then `nil` will be returned.
|
391
|
+
#
|
392
|
+
# @param [Symbol]
|
393
|
+
# @return [String,nil]
|
394
|
+
def attribute_for sym
|
395
|
+
@@array = attributes
|
396
|
+
val = @@const_map[sym]
|
397
|
+
val if attributes.include? val
|
398
|
+
end
|
399
|
+
|
400
|
+
# (see #attribute_for)
|
401
|
+
def action_for sym
|
402
|
+
@@array = actions
|
403
|
+
val = @@const_map[sym]
|
404
|
+
val if actions.include? val
|
405
|
+
end
|
406
|
+
|
407
|
+
# (see #attribute_for)
|
408
|
+
def param_attribute_for sym
|
409
|
+
@@array = param_attributes
|
410
|
+
val = @@const_map[sym]
|
411
|
+
val if param_attributes.include? val
|
412
|
+
end
|
413
|
+
|
414
|
+
##
|
415
|
+
# @private
|
416
|
+
#
|
417
|
+
# Memoized map for symbols to constants used for attribute/action
|
418
|
+
# lookups.
|
419
|
+
#
|
420
|
+
# @return [Hash{Symbol=>String}]
|
421
|
+
@@const_map = Hash.new do |hash,key|
|
422
|
+
@@array.map { |x| hash[strip_prefix(x).underscore.to_sym] = x }
|
423
|
+
if hash.has_key? key
|
424
|
+
hash[key]
|
425
|
+
else # try other cases of transformations
|
426
|
+
real_key = key.chomp('?').to_sym
|
427
|
+
hash.has_key?(real_key) ? hash[key] = hash[real_key] : nil
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
|
432
|
+
class << self
|
433
|
+
|
434
|
+
##
|
435
|
+
# Retrieve and process the value of the given attribute for the
|
436
|
+
# given element reference.
|
437
|
+
#
|
438
|
+
# @param [AXUIElementRef]
|
439
|
+
# @param [String]
|
440
|
+
def attribute_for ref, attr
|
441
|
+
process AX.attr_of_element(ref, attr)
|
442
|
+
end
|
443
|
+
|
444
|
+
##
|
445
|
+
# Retrieve and process the value of the given parameterized attribute
|
446
|
+
# for the parameter and given element reference.
|
447
|
+
#
|
448
|
+
# @param [AXUIElementRef]
|
449
|
+
# @param [String]
|
450
|
+
def param_attribute_for ref, attr, param
|
451
|
+
param = param.to_axvalue if param.kind_of? Boxed
|
452
|
+
process AX.param_attr_of_element(ref, attr, param)
|
453
|
+
end
|
454
|
+
|
455
|
+
##
|
456
|
+
# Meant for taking a return value from {AX.attr_of_element} and,
|
457
|
+
# if required, converts the data to something more usable.
|
458
|
+
#
|
459
|
+
# Generally, used to process an `AXValue` into a `CGPoint` or an
|
460
|
+
# `AXUIElementRef` into some kind of {AX::Element} object.
|
461
|
+
def process value
|
462
|
+
return nil if value.nil?
|
463
|
+
id = ATTR_MASSAGERS[CFGetTypeID(value)]
|
464
|
+
id ? self.send(id, value) : value
|
465
|
+
end
|
466
|
+
|
467
|
+
##
|
468
|
+
# @note In the case of a predicate name, this will strip the 'Is'
|
469
|
+
# part of the name if it is present
|
470
|
+
#
|
471
|
+
# Takes an accessibility constant and returns a new string with the
|
472
|
+
# namespace prefix removed.
|
473
|
+
#
|
474
|
+
# @example
|
475
|
+
#
|
476
|
+
# AX.strip_prefix 'AXTitle' # => 'Title'
|
477
|
+
# AX.strip_prefix 'AXIsApplicationEnabled' # => 'ApplicationEnabled'
|
478
|
+
# AX.strip_prefix 'MCAXEnabled' # => 'Enabled'
|
479
|
+
# AX.strip_prefix KAXWindowCreatedNotification # => 'WindowCreated'
|
480
|
+
# AX.strip_prefix NSAccessibilityButtonRole # => 'Button'
|
481
|
+
#
|
482
|
+
# @param [String] const
|
483
|
+
# @return [String]
|
484
|
+
def strip_prefix const
|
485
|
+
const.sub /^[A-Z]*?AX(?:Is)?/, ::EMPTY_STRING
|
486
|
+
end
|
487
|
+
|
488
|
+
|
489
|
+
private
|
490
|
+
|
491
|
+
##
|
492
|
+
# @private
|
493
|
+
#
|
494
|
+
# Map Core Foundation type ID numbers to methods. This is how
|
495
|
+
# double dispatch is used to massage low level data into
|
496
|
+
# something nice.
|
497
|
+
#
|
498
|
+
# Indexes are looked up and added to the array at runtime in
|
499
|
+
# case values change in the future.
|
500
|
+
#
|
501
|
+
# @return [Array<Symbol>]
|
502
|
+
ATTR_MASSAGERS = []
|
503
|
+
ATTR_MASSAGERS[AXUIElementGetTypeID()] = :process_element
|
504
|
+
ATTR_MASSAGERS[CFArrayGetTypeID()] = :process_array
|
505
|
+
ATTR_MASSAGERS[AXValueGetTypeID()] = :process_box
|
506
|
+
|
507
|
+
##
|
508
|
+
# @todo Refactor this pipeline so that we can pass the attributes we look
|
509
|
+
# up to the initializer for Element, and also so we can avoid some
|
510
|
+
# other duplicated work.
|
511
|
+
#
|
512
|
+
# Takes an AXUIElementRef and gives you some kind of accessibility object.
|
513
|
+
#
|
514
|
+
# @param [AXUIElementRef]
|
515
|
+
# @return [AX::Element]
|
516
|
+
def process_element ref
|
517
|
+
attrs = AX.attrs_of_element ref
|
518
|
+
role = AX.role_for(ref, attrs).map! { |x| strip_prefix x }
|
519
|
+
determine_class_for(role).new(ref, attrs)
|
520
|
+
end
|
521
|
+
|
522
|
+
##
|
523
|
+
# Like `#const_get` except that if the class does not exist yet then
|
524
|
+
# it will assume the constant belongs to a class and creates the class
|
525
|
+
# for you.
|
526
|
+
#
|
527
|
+
# @param [Array<String>] const the value you want as a constant
|
528
|
+
# @return [Class] a reference to the class being looked up
|
529
|
+
def determine_class_for names
|
530
|
+
klass = names.first
|
531
|
+
if AX.const_defined? klass, false
|
532
|
+
AX.const_get klass
|
533
|
+
else
|
534
|
+
create_class *names
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
##
|
539
|
+
# Creates new class at run time and puts it into the {AX} namespace.
|
540
|
+
#
|
541
|
+
# @param [String,Symbol] name
|
542
|
+
# @param [String,Symbol] superklass
|
543
|
+
# @return [Class]
|
544
|
+
def create_class name, superklass = :Element
|
545
|
+
real_superklass = determine_class_for [superklass]
|
546
|
+
klass = Class.new real_superklass
|
547
|
+
AX.const_set name, klass
|
548
|
+
end
|
549
|
+
|
550
|
+
##
|
551
|
+
# @todo Consider mapping in all cases to avoid returning a CFArray
|
552
|
+
#
|
553
|
+
# We assume a homogeneous array and only massage element arrays right now.
|
554
|
+
#
|
555
|
+
# @return [Array]
|
556
|
+
def process_array vals
|
557
|
+
return vals if vals.empty? || !ATTR_MASSAGERS[CFGetTypeID(vals.first)]
|
558
|
+
vals.map { |val| process_element val }
|
559
|
+
end
|
560
|
+
|
561
|
+
##
|
562
|
+
# Extract the stuct contained in an `AXValueRef`.
|
563
|
+
#
|
564
|
+
# @param [AXValueRef] value
|
565
|
+
# @return [Boxed]
|
566
|
+
def process_box value
|
567
|
+
box_type = AXValueGetType(value)
|
568
|
+
ptr = Pointer.new BOX_TYPES[box_type]
|
569
|
+
AXValueGetValue(value, box_type, ptr)
|
570
|
+
ptr[0]
|
571
|
+
end
|
572
|
+
|
573
|
+
##
|
574
|
+
# @private
|
575
|
+
#
|
576
|
+
# Map of type encodings used for wrapping structs when coming from
|
577
|
+
# an `AXValueRef`.
|
578
|
+
#
|
579
|
+
# The list is order sensitive, which is why we unshift nil, but
|
580
|
+
# should probably be more rigorously defined at runtime.
|
581
|
+
#
|
582
|
+
# @return [String,nil]
|
583
|
+
BOX_TYPES = [CGPoint, CGSize, CGRect, CFRange].map! { |x| x.type }.unshift(nil)
|
584
|
+
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
require 'ax_elements/elements/application'
|
589
|
+
require 'ax_elements/elements/systemwide'
|
590
|
+
require 'ax_elements/elements/row'
|
591
|
+
require 'ax_elements/elements/button'
|
592
|
+
require 'ax_elements/elements/static_text'
|
593
|
+
require 'ax_elements/elements/radio_button'
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
##
|
4
|
+
# Some additional constructors and conveniences for Application objects.
|
5
|
+
#
|
6
|
+
# As this class has evolved, it has gathered some functionality from
|
7
|
+
# the `NSRunningApplication` class.
|
8
|
+
class AX::Application < AX::Element
|
9
|
+
|
10
|
+
##
|
11
|
+
# Overridden so that we can also cache the `NSRunningApplication`
|
12
|
+
# instance for this object.
|
13
|
+
def initialize ref, attrs
|
14
|
+
super
|
15
|
+
@app = NSRunningApplication.runningApplicationWithProcessIdentifier pid
|
16
|
+
end
|
17
|
+
|
18
|
+
# @group Attributes
|
19
|
+
|
20
|
+
##
|
21
|
+
# Overridden to handle the {Accessibility::Language#set_focus} case.
|
22
|
+
def attribute attr
|
23
|
+
attr == :focused? || attr == :focused ? active? : super
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Ask the app whether or not it is the active app. This is equivalent
|
28
|
+
# to the dynamic #focused? method, but might make more sense to use
|
29
|
+
# in some cases.
|
30
|
+
def active?
|
31
|
+
NSRunLoop.currentRunLoop.runUntilDate Time.now
|
32
|
+
@app.active?
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Overridden to handle the {Accessibility::Language#set_focus} case.
|
37
|
+
def set_attribute attr, value
|
38
|
+
if attr == :focused
|
39
|
+
if value
|
40
|
+
@app.unhide
|
41
|
+
@app.activateWithOptions NSApplicationActivateIgnoringOtherApps
|
42
|
+
else
|
43
|
+
@app.hide
|
44
|
+
end
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# @group Actions
|
51
|
+
|
52
|
+
##
|
53
|
+
# @note This object becomes poisonous after the app terminates. If you
|
54
|
+
# try to use it again, you will crash MacRuby.
|
55
|
+
#
|
56
|
+
# Ask the application to terminate itself. Be careful how you use this.
|
57
|
+
#
|
58
|
+
# @return [Boolean]
|
59
|
+
def perform_action name
|
60
|
+
case name
|
61
|
+
when :terminate, :hide, :unhide
|
62
|
+
@app.send name
|
63
|
+
else
|
64
|
+
super
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Send keyboard input to `self`, the control that currently has focus
|
70
|
+
# will the control that receives the key presses.
|
71
|
+
#
|
72
|
+
# @return [nil]
|
73
|
+
def type_string string
|
74
|
+
AX.keyboard_action @ref, string
|
75
|
+
end
|
76
|
+
|
77
|
+
# @endgroup
|
78
|
+
|
79
|
+
# @todo Do we need to override #respond_to? and #methods for
|
80
|
+
# the :focused? case as well?
|
81
|
+
|
82
|
+
##
|
83
|
+
# Override the base class to make sure the pid is included.
|
84
|
+
def inspect
|
85
|
+
(super).sub />$/, "#{pp_checkbox(:focused)} pid=#{self.pid}>"
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
##
|
2
|
+
# A generic push button and the base class for most, but not all,
|
3
|
+
# other buttons, including close buttons and sort buttons, but
|
4
|
+
# not including pop-up buttons or radio buttons.
|
5
|
+
class AX::Button < AX::Element
|
6
|
+
|
7
|
+
##
|
8
|
+
# Test equality with another object. Equality can be with another
|
9
|
+
# {AX::Element} or it can be with a string that matches the title
|
10
|
+
# of the button.
|
11
|
+
#
|
12
|
+
# @return [Boolean]
|
13
|
+
def == other
|
14
|
+
return super unless other.kind_of? NSString
|
15
|
+
return attribute(:title) == other
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
##
|
2
|
+
# Radio buttons are not the same as a generic button, radio buttons work
|
3
|
+
# in mutually exclusive groups (you can only select one at a time). You
|
4
|
+
# often have radio buttons when dealing with tab groups.
|
5
|
+
class AX::RadioButton < AX::Element
|
6
|
+
|
7
|
+
##
|
8
|
+
# Test equality with another object. Equality can be with another
|
9
|
+
# {AX::Element} or it can be with a string that matches the title
|
10
|
+
# of the radio button.
|
11
|
+
#
|
12
|
+
# @return [Boolean]
|
13
|
+
def == other
|
14
|
+
return attribute(:title) == other if other.kind_of? NSString
|
15
|
+
return super
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|