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.
Files changed (54) hide show
  1. data/.yardopts +20 -0
  2. data/LICENSE.txt +25 -0
  3. data/README.markdown +150 -0
  4. data/Rakefile +109 -0
  5. data/docs/AccessibilityTips.markdown +119 -0
  6. data/docs/Acting.markdown +340 -0
  7. data/docs/Debugging.markdown +326 -0
  8. data/docs/Inspecting.markdown +255 -0
  9. data/docs/KeyboardEvents.markdown +57 -0
  10. data/docs/NewBehaviour.markdown +151 -0
  11. data/docs/Notifications.markdown +271 -0
  12. data/docs/Searching.markdown +250 -0
  13. data/docs/TestingExtensions.markdown +52 -0
  14. data/docs/images/AX.png +0 -0
  15. data/docs/images/all_the_buttons.jpg +0 -0
  16. data/docs/images/ui_hierarchy.dot +34 -0
  17. data/docs/images/ui_hierarchy.png +0 -0
  18. data/ext/key_coder/extconf.rb +6 -0
  19. data/ext/key_coder/key_coder.m +77 -0
  20. data/lib/ax_elements/accessibility/enumerators.rb +104 -0
  21. data/lib/ax_elements/accessibility/language.rb +347 -0
  22. data/lib/ax_elements/accessibility/qualifier.rb +73 -0
  23. data/lib/ax_elements/accessibility.rb +164 -0
  24. data/lib/ax_elements/core.rb +541 -0
  25. data/lib/ax_elements/element.rb +593 -0
  26. data/lib/ax_elements/elements/application.rb +88 -0
  27. data/lib/ax_elements/elements/button.rb +18 -0
  28. data/lib/ax_elements/elements/radio_button.rb +18 -0
  29. data/lib/ax_elements/elements/row.rb +30 -0
  30. data/lib/ax_elements/elements/static_text.rb +17 -0
  31. data/lib/ax_elements/elements/systemwide.rb +46 -0
  32. data/lib/ax_elements/inspector.rb +116 -0
  33. data/lib/ax_elements/macruby_extensions.rb +255 -0
  34. data/lib/ax_elements/notification.rb +37 -0
  35. data/lib/ax_elements/version.rb +9 -0
  36. data/lib/ax_elements.rb +30 -0
  37. data/lib/minitest/ax_elements.rb +19 -0
  38. data/lib/mouse.rb +185 -0
  39. data/lib/rspec/expectations/ax_elements.rb +15 -0
  40. data/test/elements/test_application.rb +72 -0
  41. data/test/elements/test_row.rb +27 -0
  42. data/test/elements/test_systemwide.rb +38 -0
  43. data/test/helper.rb +119 -0
  44. data/test/test_accessibility.rb +127 -0
  45. data/test/test_blankness.rb +26 -0
  46. data/test/test_core.rb +448 -0
  47. data/test/test_element.rb +939 -0
  48. data/test/test_enumerators.rb +81 -0
  49. data/test/test_inspector.rb +121 -0
  50. data/test/test_language.rb +157 -0
  51. data/test/test_macruby_extensions.rb +303 -0
  52. data/test/test_mouse.rb +5 -0
  53. data/test/test_search_semantics.rb +143 -0
  54. 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