AXElements 0.6.0beta2 → 0.7.5

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