AXElements 0.6.0beta2 → 0.7.5

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 (107) hide show
  1. data/.yardopts +1 -2
  2. data/README.markdown +152 -88
  3. data/Rakefile +8 -103
  4. data/docs/Debugging.markdown +9 -2
  5. data/docs/KeyboardEvents.markdown +114 -49
  6. data/docs/Setting.markdown +1 -0
  7. data/docs/images/next_version.png +0 -0
  8. data/ext/accessibility/key_coder/extconf.rb +22 -0
  9. data/ext/accessibility/key_coder/key_coder.c +113 -0
  10. data/lib/AXElements.rb +2 -0
  11. data/lib/accessibility/core.rb +897 -0
  12. data/lib/accessibility/debug.rb +168 -0
  13. data/lib/accessibility/dsl.rb +697 -0
  14. data/lib/accessibility/enumerators.rb +104 -0
  15. data/lib/accessibility/errors.rb +32 -0
  16. data/lib/accessibility/factory.rb +153 -0
  17. data/lib/accessibility/graph.rb +150 -0
  18. data/lib/{ax_elements/inspector.rb → accessibility/pp_inspector.rb} +39 -28
  19. data/lib/accessibility/qualifier.rb +158 -0
  20. data/lib/accessibility/string.rb +494 -0
  21. data/lib/accessibility/translator.rb +178 -0
  22. data/lib/accessibility/version.rb +7 -0
  23. data/lib/accessibility.rb +79 -0
  24. data/lib/ax/application.rb +234 -0
  25. data/lib/{ax_elements/elements → ax}/button.rb +2 -0
  26. data/lib/ax/element.rb +518 -0
  27. data/lib/{ax_elements/elements → ax}/radio_button.rb +2 -0
  28. data/lib/ax/row.rb +37 -0
  29. data/lib/{ax_elements/elements → ax}/static_text.rb +2 -0
  30. data/lib/ax/systemwide.rb +86 -0
  31. data/lib/ax_elements/awesome_print.rb +25 -0
  32. data/lib/ax_elements/exception_workaround.rb +8 -0
  33. data/lib/ax_elements/nsarray_compat.rb +64 -0
  34. data/lib/ax_elements/vendor/inflection_data.rb +65 -0
  35. data/lib/ax_elements/vendor/inflections.rb +172 -0
  36. data/lib/ax_elements/vendor/inflector.rb +306 -0
  37. data/lib/ax_elements.rb +14 -25
  38. data/lib/minitest/ax_elements.rb +112 -12
  39. data/lib/mouse.rb +72 -46
  40. data/lib/rspec/expectations/ax_elements.rb +133 -6
  41. data/rakelib/doc.rake +13 -0
  42. data/rakelib/ext.rake +61 -0
  43. data/rakelib/gem.rake +28 -0
  44. data/rakelib/test.rake +53 -0
  45. data/test/helper.rb +11 -97
  46. data/test/integration/accessibility/test_core.rb +18 -0
  47. data/test/integration/accessibility/test_debug.rb +44 -0
  48. data/test/integration/accessibility/test_dsl.rb +225 -0
  49. data/test/integration/accessibility/test_enumerators.rb +122 -0
  50. data/test/integration/accessibility/test_errors.rb +38 -0
  51. data/test/integration/accessibility/test_notifications.rb +22 -0
  52. data/test/integration/accessibility/test_qualifier.rb +148 -0
  53. data/test/integration/ax/test_application.rb +56 -0
  54. data/test/integration/ax/test_element.rb +46 -0
  55. data/test/integration/ax/test_row.rb +23 -0
  56. data/test/integration/ax_elements/test_nsarray_compat.rb +43 -0
  57. data/test/integration/minitest/test_ax_elements.rb +98 -0
  58. data/test/integration/rspec/expectations/test_ax_elements.rb +58 -0
  59. data/test/integration/test_mouse.rb +35 -0
  60. data/test/sanity/accessibility/test_core.rb +553 -0
  61. data/test/sanity/accessibility/test_debug.rb +63 -0
  62. data/test/sanity/accessibility/test_dsl.rb +75 -0
  63. data/test/sanity/accessibility/test_errors.rb +10 -0
  64. data/test/sanity/accessibility/test_factory.rb +88 -0
  65. data/test/sanity/accessibility/test_pp_inspector.rb +110 -0
  66. data/test/sanity/accessibility/test_qualifier.rb +13 -0
  67. data/test/sanity/accessibility/test_string.rb +238 -0
  68. data/test/sanity/accessibility/test_translator.rb +145 -0
  69. data/test/sanity/ax/test_application.rb +90 -0
  70. data/test/sanity/ax/test_element.rb +80 -0
  71. data/test/sanity/ax/test_systemwide.rb +66 -0
  72. data/test/sanity/ax_elements/test_nsarray_compat.rb +16 -0
  73. data/test/sanity/ax_elements/test_nsobject_inspect.rb +11 -0
  74. data/test/sanity/minitest/test_ax_elements.rb +15 -0
  75. data/test/sanity/rspec/expectations/test_ax_elements.rb +12 -0
  76. data/test/sanity/test_ax_elements.rb +10 -0
  77. data/test/sanity/test_mouse.rb +19 -0
  78. metadata +111 -93
  79. data/LICENSE.txt +0 -25
  80. data/ext/key_coder/extconf.rb +0 -6
  81. data/ext/key_coder/key_coder.m +0 -77
  82. data/lib/ax_elements/accessibility/enumerators.rb +0 -104
  83. data/lib/ax_elements/accessibility/graph.rb +0 -118
  84. data/lib/ax_elements/accessibility/language.rb +0 -347
  85. data/lib/ax_elements/accessibility/qualifier.rb +0 -73
  86. data/lib/ax_elements/accessibility.rb +0 -166
  87. data/lib/ax_elements/core.rb +0 -541
  88. data/lib/ax_elements/element.rb +0 -593
  89. data/lib/ax_elements/elements/application.rb +0 -88
  90. data/lib/ax_elements/elements/row.rb +0 -30
  91. data/lib/ax_elements/elements/systemwide.rb +0 -46
  92. data/lib/ax_elements/macruby_extensions.rb +0 -255
  93. data/lib/ax_elements/notification.rb +0 -37
  94. data/lib/ax_elements/version.rb +0 -9
  95. data/test/elements/test_application.rb +0 -72
  96. data/test/elements/test_row.rb +0 -27
  97. data/test/elements/test_systemwide.rb +0 -38
  98. data/test/test_accessibility.rb +0 -127
  99. data/test/test_blankness.rb +0 -26
  100. data/test/test_core.rb +0 -448
  101. data/test/test_element.rb +0 -939
  102. data/test/test_enumerators.rb +0 -81
  103. data/test/test_inspector.rb +0 -130
  104. data/test/test_language.rb +0 -157
  105. data/test/test_macruby_extensions.rb +0 -303
  106. data/test/test_mouse.rb +0 -5
  107. data/test/test_search_semantics.rb +0 -143
@@ -0,0 +1,697 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'mouse'
4
+ require 'ax/element'
5
+ require 'ax/application'
6
+ require 'ax/systemwide'
7
+ require 'accessibility'
8
+ require 'accessibility/debug'
9
+
10
+ ##
11
+ # @todo Allow the animation duration to be overridden for Mouse stuff?
12
+ #
13
+ # DSL methods for AXElements.
14
+ #
15
+ # The idea here is to pull actions out from an object and put them
16
+ # in front of object to give AXElements more of a DSL feel to make
17
+ # communicating test steps more clear. See the
18
+ # {file:docs/Acting.markdown Acting tutorial} for examples on how to use
19
+ # methods from this module.
20
+ module Accessibility::DSL
21
+
22
+
23
+ # @group Actions
24
+
25
+ ##
26
+ # We assume that any method that has the first argument with a type
27
+ # of {AX::Element} is intended to be an action and so `#method_missing`
28
+ # will forward the message to the element.
29
+ #
30
+ # @param [String] method an action constant
31
+ def method_missing meth, *args
32
+ arg = args.first
33
+ if arg.kind_of? AX::Element
34
+ return arg.perform meth if arg.actions.include? meth
35
+ raise ArgumentError, "`#{meth}' is not an action of #{self}:#{self.class}"
36
+ end
37
+ # @todo do we still need this? we should just call super
38
+ # should be able to just call super, but there is a bug in MacRuby (#1320)
39
+ # so we just recreate what should be happening
40
+ message = "undefined method `#{meth}' for #{self}:#{self.class}"
41
+ raise NoMethodError, message, caller(1)
42
+ end
43
+
44
+ ##
45
+ # Try to perform the `press` action on the given element.
46
+ #
47
+ # @param [AX::Element]
48
+ # @return [Boolean]
49
+ def press element
50
+ element.perform :press
51
+ end
52
+
53
+ ##
54
+ # Try to perform the `show_menu` action on the given element.
55
+ #
56
+ # @param [AX::Element]
57
+ # @return [Boolean]
58
+ def show_menu element
59
+ element.perform :show_menu
60
+ end
61
+
62
+ ##
63
+ # Try to perform the `pick` action on the given element.
64
+ #
65
+ # @param [AX::Element]
66
+ # @return [Boolean]
67
+ def pick element
68
+ element.perform :pick
69
+ end
70
+
71
+ ##
72
+ # Try to perform the `decrement` action on the given element.
73
+ #
74
+ # @param [AX::Element]
75
+ # @return [Boolean]
76
+ def decrement element
77
+ element.perform :decrement
78
+ end
79
+
80
+ ##
81
+ # Try to perform the `confirm` action on the given element.
82
+ #
83
+ # @param [AX::Element]
84
+ # @return [Boolean]
85
+ def confirm element
86
+ element.perform :confirm
87
+ end
88
+
89
+ ##
90
+ # Try to perform the `increment` action on the given element.
91
+ #
92
+ # @param [AX::Element]
93
+ # @return [Boolean]
94
+ def increment element
95
+ element.perform :increment
96
+ end
97
+
98
+ ##
99
+ # Try to perform the `delete` action on the given element.
100
+ #
101
+ # @param [AX::Element]
102
+ # @return [Boolean]
103
+ def delete element
104
+ element.perform :delete
105
+ end
106
+
107
+ ##
108
+ # Try to perform the `cancel` action on the given element.
109
+ #
110
+ # @param [AX::Element]
111
+ # @return [Boolean]
112
+ def cancel element
113
+ element.perform :cancel
114
+ end
115
+
116
+ ##
117
+ # Tell an app to hide itself.
118
+ #
119
+ # @param [AX::Application]
120
+ # @return [Boolean]
121
+ def hide app
122
+ app.perform :hide
123
+ end
124
+
125
+ ##
126
+ # Tell an app to unhide itself. This does not guarantee it will be
127
+ # focused.
128
+ #
129
+ # @param [AX::Application]
130
+ # @return [Boolean]
131
+ def unhide app
132
+ app.perform :unhide
133
+ end
134
+ alias_method :show, :unhide
135
+
136
+ ##
137
+ # Tell an app to quit.
138
+ #
139
+ # @param [AX::Application]
140
+ # @return [Boolean]
141
+ def terminate app
142
+ app.perform :terminate
143
+ end
144
+
145
+ ##
146
+ # Find the application with the given bundle identifier.
147
+ # If the application is not already running, it will be
148
+ # launched.
149
+ #
150
+ # @example
151
+ #
152
+ # app_with_identifier 'com.apple.finder'
153
+ #
154
+ # @param [String]
155
+ # @return [AX::Application]
156
+ def app_with_bundle_identifier id
157
+ Accessibility.application_with_bundle_identifier id
158
+ end
159
+ alias_method :app_with_bundle_id, :app_with_bundle_identifier
160
+ alias_method :launch, :app_with_bundle_identifier
161
+
162
+ ##
163
+ # Find the application with the given name.
164
+ #
165
+ # @example
166
+ #
167
+ # app_with_name 'Finder'
168
+ #
169
+ # @param [String]
170
+ # @return [AX::Application,nil]
171
+ def app_with_name name
172
+ AX::Application.new name
173
+ end
174
+
175
+ ##
176
+ # Find the application with the given process identifier.
177
+ #
178
+ # @example
179
+ #
180
+ # app_with_pid 35843
181
+ #
182
+ # @param [Fixnum]
183
+ # @return [AX::Application]
184
+ def app_with_pid pid
185
+ AX::Application.new pid
186
+ end
187
+
188
+ ##
189
+ # @note This method overrides `Kernel#raise` so we have to check the
190
+ # class of the first argument to decide which code path to take.
191
+ #
192
+ # Try to perform the `raise` action on the given element.
193
+ #
194
+ # @overload raise element
195
+ # @param [AX::Element] element
196
+ # @return [Boolean]
197
+ #
198
+ # @overload raise exception[, message[, backtrace]]
199
+ # The normal way to raise an exception.
200
+ def raise *args
201
+ arg = args.first
202
+ # @todo Need to check if arg has the raise action
203
+ arg.kind_of?(AX::Element) ? arg.perform(:raise) : super
204
+ end
205
+
206
+ ##
207
+ # Focus an element on the screen if it can be focused. It is safe to
208
+ # pass any element into this method as nothing will happen if it is
209
+ # not capable of having focus set on it.
210
+ #
211
+ # @param [AX::Element]
212
+ def set_focus_to element
213
+ element.set(:focused, true) if element.respond_to? :focused?
214
+ end
215
+ alias_method :set_focus, :set_focus_to
216
+
217
+ ##
218
+ # Set the value of an attribute on an element.
219
+ #
220
+ # This method will try to set focus to the element first; this is
221
+ # to avoid cases where developers assumed an element would have
222
+ # to have focus before a user could change the value.
223
+ #
224
+ # @overload set element, attribute_name: new_value
225
+ # Set a specified attribute to a new value
226
+ # @param [AX::Element] element
227
+ # @param [Hash{attribute_name=>new_value}] change
228
+ #
229
+ # @example
230
+ #
231
+ # set text_field, selected_text_range: CFRangeMake(1,10)
232
+ #
233
+ # @overload set element, new_value
234
+ # Set the `value` attribute to a new value
235
+ # @param [AX::Element] element
236
+ # @param [Object] change
237
+ #
238
+ # @example
239
+ #
240
+ # set text_field, 'Mark Rada'
241
+ # set radio_button, 1
242
+ #
243
+ # @return [nil] do not rely on a return value
244
+ def set element, change
245
+ if element.respond_to? :focused
246
+ if element.attribute_writable? :focused
247
+ element.set :focused, true
248
+ end
249
+ end
250
+
251
+ return element.set :value, change unless change.kind_of? Hash
252
+ key, value = change.first
253
+ return element.set key, value
254
+ end
255
+
256
+ ##
257
+ # Simulate keyboard input by typing out the given string. To learn
258
+ # more about how to encode modifier keys (e.g. Command), see the
259
+ # dedicated documentation page on
260
+ # {file:docs/KeyboardEvents.markdown Keyboard Events}.
261
+ #
262
+ # @overload type string
263
+ # Send input to the currently focused application
264
+ # @param [#to_s]
265
+ #
266
+ # @overload type string, app
267
+ # Send input to a specific application
268
+ # @param [#to_s]
269
+ # @param [AX::Application]
270
+ def type string, app = system_wide
271
+ sleep 0.1
272
+ app.type_string string.to_s
273
+ end
274
+
275
+ ##
276
+ # Navigate the menu bar menus for the given application and select
277
+ # the last item in the chain.
278
+ #
279
+ # @example
280
+ #
281
+ # mail = app_with_name 'Mail'
282
+ # select_menu_item mail, 'View', 'Sort By', 'Subject'
283
+ # select_menu_item mail, 'Edit', /Spelling/, /show spelling/i
284
+ #
285
+ # @param [AX::Application]
286
+ # @param [String,Regexp] path
287
+ # @return [Boolean]
288
+ def select_menu_item app, *path
289
+ app.select_menu_item *path
290
+ end
291
+
292
+
293
+ # @group Notifications
294
+
295
+ ##
296
+ # Register for a notification from a specific element.
297
+ #
298
+ # @param [#to_s]
299
+ # @param [Array(#to_s,AX::Element)]
300
+ def register_for notif, from: element, &block
301
+ @registered_elements ||= []
302
+ @registered_elements << element
303
+ element.on_notification notif, &block
304
+ end
305
+
306
+ ##
307
+ # @deprecated This API exists for backwards compatability only
308
+ #
309
+ # Register for a notification from a specific element.
310
+ #
311
+ # @param [AX::Element]
312
+ # @param [String]
313
+ def register_for_notification element, notif, &block
314
+ register_for notif, from: element, &block
315
+ end
316
+
317
+ ##
318
+ # Pause script execution until notification that has been registered
319
+ # for is received or the full timeout period has passed.
320
+ #
321
+ # If the script is unpaused because of a timeout, then it is assumed
322
+ # that the notification was never received and all notification
323
+ # registrations will be unregistered to avoid future complications.
324
+ #
325
+ # @param [Float] timeout number of seconds to wait for a notification
326
+ # @return [Boolean]
327
+ def wait_for_notification timeout = 10.0
328
+ # We use RunInMode because it has timeout functionality
329
+ case CFRunLoopRunInMode(KCFRunLoopDefaultMode, timeout, false)
330
+ when KCFRunLoopRunStopped then true
331
+ when KCFRunLoopRunTimedOut then false.tap { |_| unregister_notifications }
332
+ when KCFRunLoopFinished then
333
+ raise RuntimeError, 'The run loop was not configured properly'
334
+ when KCFRunLoopRunHandledSource then
335
+ raise RuntimeError, 'Did you start your own run loop?'
336
+ else
337
+ raise 'You just found a bug, might be yours, or OS X, or MacRuby...'
338
+ end
339
+ end
340
+
341
+ ##
342
+ # Undo _all_ notification registries.
343
+ def unregister_notifications
344
+ return unless @registered_elements
345
+ @registered_elements.each do |element|
346
+ element.unregister_notifications
347
+ end
348
+ @registered_elements = []
349
+ end
350
+
351
+
352
+ # @group Polling
353
+
354
+ ##
355
+ # Simply wait around for something to show up. This method is similar to
356
+ # performing an explicit search on an element except that the search filters
357
+ # take two extra options which can control how long to wait and from where
358
+ # to start searches from. You __MUST__ supply either the parent or ancestor
359
+ # options to specify where to search from. Searching from the parent implies
360
+ # that what you are waiting for is a child of the parent and not a more
361
+ # distant descendant.
362
+ #
363
+ # This is an alternative to using the notifications system. It is far
364
+ # easier to use than notifications in most cases, but it will perform
365
+ # more slowly (and without all the fun crashes).
366
+ #
367
+ # @example
368
+ #
369
+ # # Waiting for a dialog window to show up
370
+ # wait_for :dialog, parent: app
371
+ #
372
+ # # Waiting for a hypothetical email from Mark Rada to appear
373
+ # wait_for :static_text, value: 'Mark Rada', ancestor: mail.main_window
374
+ #
375
+ # # Waiting for something that will never show up
376
+ # wait_for :a_million_dollars, ancestor: fruit_basket, timeout: 1000000
377
+ #
378
+ # @param [#to_s]
379
+ # @param [Hash] opts
380
+ # @options opts [Number] :timeout (15)
381
+ # @options opts [AX::Element] :parent
382
+ # @options opts [AX::Element] :ancestor
383
+ # @return [AX::Element,nil]
384
+ def wait_for element, opts = {}, &block
385
+ if opts.has_key? :ancestor
386
+ wait_for_descendant element, opts.delete(:ancestor), opts, &block
387
+ elsif opts.has_key? :parent
388
+ wait_for_child element, opts.delete(:parent), opts, &block
389
+ else
390
+ raise ArgumentError, 'parent/ancestor opt required'
391
+ end
392
+ end
393
+
394
+ ##
395
+ # Wait around for particular element and then return that element.
396
+ # The options you pass to this method can be any search filter that
397
+ # you can normally use.
398
+ #
399
+ # @param [#to_s]
400
+ # @param [AX::Element]
401
+ # @param [Hash]
402
+ # @return [AX::Element,nil]
403
+ def wait_for_descendant descendant, ancestor, opts, &block
404
+ timeout = opts.delete(:timeout) || 15
405
+ start = Time.now
406
+ until Time.now - start > timeout
407
+ result = ancestor.search(descendant, opts, &block)
408
+ return result unless result.blank?
409
+ sleep 0.2
410
+ end
411
+ nil
412
+ end
413
+ alias_method :wait_for_descendent, :wait_for_descendant
414
+
415
+ ##
416
+ # @note This is really just an optimized case of
417
+ # {#wait_for_descendant} when you know what you are waiting
418
+ # for is a child of a particular element.
419
+ #
420
+ # Wait around for particular element and then return that element.
421
+ # The parent option must be the parent of the element you are
422
+ # waiting for, this method will not look further down the hierarchy.
423
+ # The options you pass to this method can be any search filter that
424
+ # you can normally use.
425
+ #
426
+ # @param [#to_s]
427
+ # @param [AX::Element]
428
+ # @param [Hash]
429
+ # @return [AX::Element,nil]
430
+ def wait_for_child child, parent, opts, &block
431
+ timeout = opts.delete(:timeout) || 15
432
+ start = Time.now
433
+ q = Accessibility::Qualifier.new(child, opts, &block)
434
+ until Time.now - start > timeout
435
+ result = parent.attribute(:children).find { |x| q.qualifies? x }
436
+ return result unless result.blank?
437
+ sleep 0.2
438
+ end
439
+ nil
440
+ end
441
+
442
+
443
+ # @group Mouse Interaction
444
+
445
+ ##
446
+ # Move the mouse cursor to the given point on the screen.
447
+ #
448
+ # @example
449
+ #
450
+ # move_mouse_to button
451
+ # move_mouse_to [344, 516]
452
+ # move_mouse_to CGPointMake(100, 100)
453
+ #
454
+ # @param [#to_point]
455
+ # @param [Hash] opts
456
+ # @option opts [Number] :duration
457
+ # @option opts [Number] :wait
458
+ def move_mouse_to arg, opts = {}
459
+ duration = opts[:duration] || 0.2
460
+ if Accessibility::Debug.on? && arg.respond_to?(:bounds)
461
+ highlight arg, timeout: duration, color: NSColor.orangeColor
462
+ end
463
+ Mouse.move_to arg.to_point, duration
464
+ sleep(opts[:wait] || 0.2)
465
+ end
466
+
467
+ ##
468
+ # Click and drag the mouse from its current position to the given
469
+ # position.
470
+ #
471
+ # There are many reasons why you would want to cause a drag event
472
+ # with the mouse. Perhaps you want to drag an object to another
473
+ # place, or maybe you want to hightlight an area of the screen.
474
+ #
475
+ # @param [#to_point]
476
+ def drag_mouse_to arg, opts = {}
477
+ Mouse.drag_to arg.to_point, (opts[:duration] || 0.2)
478
+ sleep(opts[:wait] || 0.2)
479
+ end
480
+
481
+ ##
482
+ # Move the mouse to the first point and then drag to the second point.
483
+ #
484
+ # @param [#to_point]
485
+ # @param [#to_point]
486
+ def drag arg1, to: arg2
487
+ move_mouse_to obj
488
+ drag_mouse_to obj2
489
+ end
490
+
491
+
492
+ ##
493
+ # @todo Need to expose the units option? Would allow scrolling by pixel.
494
+ #
495
+ # Scrolls an arbitrary number of lines at the mouses current point on
496
+ # the screen. Use a positive number to scroll down, and a negative number
497
+ # to scroll up.
498
+ #
499
+ # If the second argument is provided then the mouse will move to that
500
+ # point first; the argument must respond to `#to_point`.
501
+ #
502
+ # @param [Number]
503
+ # @param [#to_point]
504
+ def scroll lines, obj = nil, wait = 0.1
505
+ move_mouse_to obj, wait: 0 if obj
506
+ Mouse.scroll lines
507
+ sleep wait
508
+ end
509
+
510
+ ##
511
+ # Perform a regular click.
512
+ #
513
+ # If an argument is provided then the mouse will move to that point
514
+ # first; the argument must respond to `#to_point`.
515
+ #
516
+ # @param [#to_point]
517
+ def click obj = nil, wait = 0.2
518
+ move_mouse_to obj, wait: 0 if obj
519
+ Mouse.click
520
+ sleep wait
521
+ end
522
+
523
+ ##
524
+ # Perform a right (aka secondary) click action.
525
+ #
526
+ # If an argument is provided then the mouse will move to that point
527
+ # first; the argument must respond to `#to_point`.
528
+ #
529
+ # @param [#to_point]
530
+ def right_click obj = nil, wait = 0.2
531
+ move_mouse_to obj, wait: 0 if obj
532
+ Mouse.right_click
533
+ sleep wait
534
+ end
535
+ alias_method :secondary_click, :right_click
536
+
537
+ ##
538
+ # Perform a double click action.
539
+ #
540
+ # If an argument is provided then the mouse will move to that point
541
+ # first; the argument must respond to `#to_point`.
542
+ #
543
+ # @param [#to_point]
544
+ def double_click obj = nil, wait = 0.2
545
+ move_mouse_to obj, wait: 0 if obj
546
+ Mouse.double_click
547
+ sleep wait
548
+ end
549
+
550
+
551
+ # @group Debug
552
+
553
+ def highlight element, opts = {}
554
+ Accessibility::Debug.highlight element, opts
555
+ end
556
+
557
+ def path_for element
558
+ Accessibility::Debug.path element
559
+ end
560
+
561
+ def subtree_for element
562
+ # @todo Create Element#descendants
563
+ Accessibility::Debug.text_subtree element
564
+ end
565
+
566
+ ##
567
+ # @note This is an unfinished feature
568
+ #
569
+ # Make a `dot` format graph of the tree, meant for graphing with
570
+ # GraphViz.
571
+ #
572
+ # @return [String]
573
+ def graph element, open = true
574
+ Accessibility::Debug.graph_subtree element
575
+ # @todo Use the `open` flag to decide if it should be sent to
576
+ # graphviz and opened right away
577
+ end
578
+
579
+
580
+ # @group Misc.
581
+
582
+ ##
583
+ # Convenience for `AX::SystemWide.new`.
584
+ #
585
+ # @return [AX::SystemWide]
586
+ def system_wide
587
+ AX::SystemWide.new
588
+ end
589
+
590
+
591
+ # @group Macros
592
+
593
+ ##
594
+ # Get the current mouse position and return the top most element at
595
+ # that point.
596
+ #
597
+ # @return [AX::Element]
598
+ def element_under_mouse
599
+ element_at_point Mouse.current_position
600
+ end
601
+
602
+ ##
603
+ # Get the top most object at an arbitrary point on the screen for
604
+ # the given application. The given point can be a CGPoint, an Array,
605
+ # or anything else that responds to `#to_point`.
606
+ #
607
+ # @param [#to_point]
608
+ # @return [AX::Element]
609
+ def element_at_point point, for: app
610
+ app.element_at point
611
+ end
612
+
613
+ ##
614
+ # Get the top most object at an arbitrary point on the screen. The
615
+ # given point can be a CGPoint, an Array, or anything else that
616
+ # responds to `#to_point`.
617
+ #
618
+ # @param [#to_point]
619
+ def element_at_point point
620
+ system_wide.element_at point
621
+ end
622
+
623
+ ##
624
+ # Show the "About" window for an app. Returns the window that is
625
+ # opened.
626
+ #
627
+ # @param [AX::Application]
628
+ # @return [AX::Window]
629
+ def show_about_window_for app
630
+ app.show_about_window
631
+ end
632
+
633
+ ##
634
+ # @note This method assumes that the app has setup the standard
635
+ # CMD+, hotkey to open the pref window
636
+ #
637
+ # Try to open the preferences for an app. Returns the window that
638
+ # is opened.
639
+ #
640
+ # @param [AX::Application]
641
+ # @return [AX::Window]
642
+ def show_preferences_window_for app
643
+ app.show_preferences_window
644
+ end
645
+
646
+ ##
647
+ # Scroll though a table until the given element is visible.
648
+ #
649
+ # If you need to scroll an unknown ammount of units through a scroll area
650
+ # you can just pass the element that you need visible and this method
651
+ # will scroll to it for you.
652
+ #
653
+ # @param [AX::Element]
654
+ # @return [Boolean]
655
+ def scroll_to element
656
+ scroll_area = element.ancestor :scroll_area
657
+
658
+ return if NSContainsRect(scroll_area.bounds, element.bounds)
659
+ move_mouse_to scroll_area
660
+ # calculate direction to scroll
661
+ direction = element.position.y > scroll_area.position.y ? -5 : 5
662
+ until NSContainsRect(scroll_area.bounds, element.bounds)
663
+ Mouse.scroll direction
664
+ end
665
+ sleep 0.1
666
+ end
667
+
668
+ ##
669
+ # Scroll a popup menu to an item in the menu and then move the
670
+ # mouse pointer to that item.
671
+ #
672
+ # @param [AX::Element]
673
+ # @return [Boolean]
674
+ def scroll_menu_to element
675
+ menu = element.ancestor :menu
676
+ move_mouse_to menu
677
+
678
+ direction = element.position.y > menu.position.y ? -5 : 5
679
+ until NSContainsRect(menu.bounds, element.bounds)
680
+ Mouse.scroll direction
681
+ end
682
+
683
+ start = Time.now
684
+ until Time.now - start > 5
685
+ # This can happen sometimes with the little arrow bars
686
+ # in menus covering up the menu item.
687
+ if element_under_mouse.kind_of? AX::Menu
688
+ scroll direction
689
+ elsif element_under_mouse != element
690
+ move_mouse_to element
691
+ else
692
+ break
693
+ end
694
+ end
695
+ end
696
+
697
+ end