AXElements 0.6.0beta1
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 +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
|