AXElements 0.6.0beta1

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