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,178 @@
1
+ require 'accessibility/version'
2
+ require 'ax_elements/vendor/inflector'
3
+
4
+ framework 'ApplicationServices'
5
+
6
+ unless Object.const_defined? :KAXIdentifierAttribute
7
+ ##
8
+ # Added for backwards compatability with Snow Leopard.
9
+ # This attribute is standard with Lion and newer. AXElements depends
10
+ # on it being defined.
11
+ #
12
+ # @return [String]
13
+ KAXIdentifierAttribute = 'AXIdentifier'
14
+ end
15
+
16
+
17
+ ##
18
+ # Maintain all the rules for transforming Cocoa constants into something
19
+ # a little more Rubyish.
20
+ class Accessibility::Translator
21
+
22
+ def self.instance
23
+ @instance ||= new
24
+ end
25
+
26
+ ##
27
+ # Initialize the caches.
28
+ def initialize
29
+ init_unprefixes
30
+ init_normalizations
31
+ init_rubyisms
32
+ init_classifications
33
+ init_singularizations
34
+ end
35
+
36
+ ##
37
+ # @note In the case of a predicate name, this will strip the 'Is'
38
+ # part of the name if it is present
39
+ #
40
+ # Takes an accessibility constant and returns a new string with the
41
+ # namespace prefix removed.
42
+ #
43
+ # @example
44
+ #
45
+ # unprefix 'AXTitle' # => 'Title'
46
+ # unprefix 'AXIsApplicationEnabled' # => 'ApplicationEnabled'
47
+ # unprefix 'MCAXEnabled' # => 'Enabled'
48
+ # unprefix KAXWindowCreatedNotification # => 'WindowCreated'
49
+ # unprefix NSAccessibilityButtonRole # => 'Button'
50
+ #
51
+ # @param [String]
52
+ # @return [String]
53
+ def unprefix key
54
+ @unprefixes[key]
55
+ end
56
+
57
+ ##
58
+ # Given a symbol, return the equivalent accessibility constant.
59
+ #
60
+ # @param [#to_sym]
61
+ # @param [Array<String>]
62
+ # @return [String]
63
+ def lookup key, values
64
+ @values = values
65
+ @rubyisms[key.to_sym]
66
+ end
67
+
68
+ # @return [Array<Symbol>]
69
+ def rubyize keys
70
+ keys.map { |x| @normalizations[x] }
71
+ end
72
+
73
+ ##
74
+ # Try to turn an arbitrary symbol into a notification constant, and
75
+ # then get the value of the constant.
76
+ #
77
+ # @param [#to_s]
78
+ # @return [String]
79
+ def guess_notification_for name
80
+ name = name.to_s.gsub /(?:^|_)(.)/ do $1.upcase! || $1 end
81
+ const = "KAX#{name}Notification"
82
+ Object.const_defined?(const) ? Object.const_get(const) : name
83
+ end
84
+
85
+ ##
86
+ # Get the class name equivalent for a given symbol or string. This
87
+ # is just a caching front end to the `#classify` method from the
88
+ # ActiveSupport inflector.
89
+ #
90
+ # @example
91
+ #
92
+ # classify 'text_field' # => "TextField"
93
+ # classify 'buttons' # => "Button"
94
+ #
95
+ # @param [String]
96
+ # @return [String]
97
+ def classify klass
98
+ @classifications[klass]
99
+ end
100
+
101
+ ##
102
+ # Get the singularized version of the word passed in. This is just
103
+ # a caching front end to the `#singularize` method from the
104
+ # ActiveSupport inflector.
105
+ #
106
+ # @example
107
+ #
108
+ # singularize 'buttons' # => 'button'
109
+ # singularize 'check_boxes' # => 'check_box'
110
+ #
111
+ # @param [String]
112
+ # @return [String]
113
+ def singularize klass
114
+ @singularizations[klass]
115
+ end
116
+
117
+
118
+ private
119
+
120
+ # @return [Hash{String=>String}]
121
+ def init_unprefixes
122
+ @unprefixes = Hash.new do |hash, key|
123
+ hash[key] = key.sub /^[A-Z]*?AX(?:Is)?|\s+/, EMPTY_STRING
124
+ end
125
+ end
126
+
127
+ # @return [Hash{String=>Symbol}]
128
+ def init_normalizations
129
+ @normalizations = Hash.new do |hash, key|
130
+ hash[key] = Accessibility::Inflector.underscore(@unprefixes[key]).to_sym
131
+ end
132
+ end
133
+
134
+ # @return [Hash{Symbol=>String}]
135
+ def init_rubyisms
136
+ @rubyisms = Hash.new do |hash, key|
137
+ @values.each do |v| hash[@normalizations[v]] = v end
138
+ hash.fetch(key) do |k|
139
+ chomped_key = k.chomp(QUESTION_MARK).to_sym
140
+ chomped_val = hash.fetch(chomped_key, nil)
141
+ hash[key] = chomped_val if chomped_val
142
+ end
143
+ end
144
+ # preload the table
145
+ @rubyisms[:id] = KAXIdentifierAttribute
146
+ @rubyisms[:placeholder] = KAXPlaceholderValueAttribute
147
+ end
148
+
149
+ # @return [Hash{String=>String}]
150
+ def init_classifications
151
+ @classifications = Hash.new do |hash, key|
152
+ hash[key] = Accessibility::Inflector.classify(key)
153
+ end
154
+ end
155
+
156
+ # @return [Hash{String=>String}]
157
+ def init_singularizations
158
+ @singularizations = Hash.new do |hash, key|
159
+ hash[key] = Accessibility::Inflector.singularize(key)
160
+ end
161
+ end
162
+
163
+ ##
164
+ # @private
165
+ #
166
+ # Cached for performance.
167
+ #
168
+ # @return [String]
169
+ EMPTY_STRING = ''
170
+
171
+ ##
172
+ # @private
173
+ #
174
+ # Performance hack.
175
+ #
176
+ # @return [String]
177
+ QUESTION_MARK = '?'
178
+ end
@@ -0,0 +1,7 @@
1
+ module Accessibility
2
+ # @return [String]
3
+ VERSION = '0.7.5'
4
+
5
+ # @return [String]
6
+ CODE_NAME = 'Clefairy'
7
+ end
@@ -0,0 +1,79 @@
1
+ require 'ax/application'
2
+
3
+ ##
4
+ # The main AXElements namespace.
5
+ module Accessibility
6
+ class << self
7
+
8
+ # @group Finding an application object
9
+
10
+ ##
11
+ # @todo Move to {AX::Aplication#initialize} eventually.
12
+ # @todo Find a way for this method to work without sleeping;
13
+ # consider looping begin/rescue/end until AX starts up
14
+ # @todo This needs to handle bad bundle identifier's gracefully
15
+ #
16
+ # This is the standard way of creating an application object. It will
17
+ # launch the app if it is not already running and then create the
18
+ # accessibility object.
19
+ #
20
+ # However, this method is a _HUGE_ hack in cases where the app is not
21
+ # already running; I've tried to register for notifications, launch
22
+ # synchronously, etc., but there is always a problem with accessibility
23
+ # not being ready.
24
+ #
25
+ # If this method fails to find an app with the appropriate bundle
26
+ # identifier then it will return nil, eventually.
27
+ #
28
+ # @example
29
+ #
30
+ # application_with_bundle_identifier 'com.apple.mail' # wait a few seconds
31
+ # application_with_bundle_identifier 'com.marketcircle.Daylite'
32
+ #
33
+ # @param [String] bundle a bundle identifier
34
+ # @return [AX::Application,nil]
35
+ def application_with_bundle_identifier bundle
36
+ 10.times do
37
+ apps = NSRunningApplication.runningApplicationsWithBundleIdentifier bundle
38
+ return AX::Application.new(apps.first.processIdentifier) unless apps.empty?
39
+ launch_application bundle
40
+ sleep 2
41
+ end
42
+ nil
43
+ end
44
+
45
+ ##
46
+ # @deprecated Use {AX::Application.new} instead.
47
+ #
48
+ # Get the accessibility object for an application given its localized
49
+ # name. This will only work if the application is already running.
50
+ #
51
+ # @example
52
+ #
53
+ # application_with_name 'Mail'
54
+ #
55
+ # @param [String] name name of the application to launch
56
+ # @return [AX::Application,nil]
57
+ def application_with_name name
58
+ AX::Application.new name
59
+ end
60
+
61
+ # @endgroup
62
+
63
+
64
+ private
65
+
66
+ ##
67
+ # Asynchronously launch an application given the bundle identifier.
68
+ #
69
+ # @param [String] bundle the bundle identifier for the app
70
+ # @return [Boolean]
71
+ def launch_application bundle
72
+ NSWorkspace.sharedWorkspace.launchAppWithBundleIdentifier bundle,
73
+ options: NSWorkspaceLaunchAsync,
74
+ additionalEventParamDescriptor: nil,
75
+ launchIdentifier: nil
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,234 @@
1
+ require 'ax/element'
2
+ require 'accessibility/string'
3
+
4
+ ##
5
+ # Some additional constructors and conveniences for Application objects.
6
+ #
7
+ # As this class has evolved, it has gathered some functionality from
8
+ # the `NSRunningApplication` class.
9
+ class AX::Application < AX::Element
10
+ include Accessibility::String
11
+
12
+ ##
13
+ # Overridden so that we can also cache the `NSRunningApplication`
14
+ # instance for this object.
15
+ #
16
+ # You can initialize an application object with either the process
17
+ # identifier (pid) of the application, the name of the application,
18
+ # an `NSRunningApplication` instance for the application, or an
19
+ # accessibility (`AXUIElementRef`) token.
20
+ def initialize arg
21
+ case arg
22
+ when Fixnum
23
+ super SYSTEMWIDE.application_for arg
24
+ @app = NSRunningApplication.runningApplicationWithProcessIdentifier arg
25
+ when String
26
+ SYSTEMWIDE.spin_run_loop
27
+ @app = NSWorkspace.sharedWorkspace.runningApplications
28
+ .find { |app| app.localizedName == arg }
29
+ super SYSTEMWIDE.application_for @app.processIdentifier
30
+ when NSRunningApplication
31
+ super SYSTEMWIDE.application_for arg.processIdentifier
32
+ @app = arg
33
+ else
34
+ super arg # assume it is an AXUIElementRef
35
+ @app = NSRunningApplication.runningApplicationWithProcessIdentifier pid
36
+ end
37
+ end
38
+
39
+
40
+ # @group Attributes
41
+
42
+ ##
43
+ # Overridden to handle the {Accessibility::Language#set_focus} case.
44
+ #
45
+ # (see AX::Element#attribute)
46
+ def attribute attr
47
+ case attr
48
+ when :focused?, :focused then active?
49
+ when :hidden?, :hidden then hidden?
50
+ else
51
+ super
52
+ end
53
+ end
54
+
55
+ ##
56
+ # Ask the app whether or not it is the active app. This is equivalent
57
+ # to the dynamic #focused? method, but might make more sense to use
58
+ # in some cases.
59
+ def active?
60
+ @ref.spin_run_loop
61
+ @app.active?
62
+ end
63
+ alias_method :focused, :active?
64
+ alias_method :focused?, :active?
65
+
66
+ ##
67
+ # Ask the app whether or not it is hidden.
68
+ def hidden?
69
+ @ref.spin_run_loop
70
+ @app.hidden?
71
+ end
72
+
73
+ ##
74
+ # Ask the app whether or not it is still running.
75
+ def terminated?
76
+ @ref.spin_run_loop
77
+ @app.terminated?
78
+ end
79
+
80
+ ##
81
+ # Overridden to handle the {Accessibility::Language#set_focus} case.
82
+ #
83
+ # (see AX::Element#set:to:)
84
+ def set attr, to: value
85
+ case attr
86
+ when :focused
87
+ perform(value ? :unhide : :hide)
88
+ when :active, :hidden
89
+ perform(value ? :hide : :unhide)
90
+ else
91
+ super
92
+ end
93
+ end
94
+
95
+
96
+ # @group Actions
97
+
98
+ ##
99
+ # Overridden to provide extra actions (e.g. `hide`, `terminate`).
100
+ #
101
+ # (see AX::Element#perform)
102
+ #
103
+ # @return [Boolean]
104
+ def perform name
105
+ case name
106
+ when :terminate
107
+ return true if terminated?
108
+ @app.terminate; sleep 0.2; terminated?
109
+ when :force_terminate
110
+ return true if terminated?
111
+ @app.forceTerminate; sleep 0.2; terminated?
112
+ when :hide
113
+ return true if hidden?
114
+ @app.hide; sleep 0.2; hidden?
115
+ when :unhide
116
+ return true if active?
117
+ @app.activateWithOptions(NSApplicationActivateIgnoringOtherApps)
118
+ sleep 0.2; active?
119
+ else
120
+ super
121
+ end
122
+ end
123
+
124
+ ##
125
+ # Send keyboard input to `self`, the control in the app that currently
126
+ # has focus will receive the key presses.
127
+ #
128
+ # For details on how to format the string, check out the
129
+ # {file:docs/KeyboardEvents.markdown Keyboard} documentation.
130
+ #
131
+ # @return [Boolean]
132
+ def type_string string
133
+ @ref.post keyboard_events_for string
134
+ true
135
+ end
136
+
137
+ # @todo doc and cleanup
138
+ def keydown key
139
+ @ref.post [[EventGenerator::CUSTOM[key], true]]
140
+ true
141
+ end
142
+
143
+ # @todo doc and cleanup
144
+ def keyup key
145
+ @ref.post [[EventGenerator::CUSTOM[key], false]]
146
+ true
147
+ end
148
+
149
+ # @return [AX::MenuItem]
150
+ def select_menu_item *path
151
+ target = navigate_menu *path
152
+ target.perform :press
153
+ target
154
+ end
155
+
156
+ # @return [AX::MenuItem]
157
+ def navigate_menu *path
158
+ perform :unhide # can't navigate menus unless the app is up front
159
+ current = attribute(:menu_bar).search(:menu_bar_item, title: path.shift)
160
+ path.each do |part|
161
+ current.perform :press
162
+ next_item = current.search(:menu_item, title: part)
163
+ if next_item.blank?
164
+ failure = Accessibility::SearchFailure.new(current, :menu_item, title: part)
165
+ current.perform :cancel # close menu
166
+ raise failure
167
+ else
168
+ current = next_item
169
+ end
170
+ end
171
+ current
172
+ end
173
+
174
+ ##
175
+ # Show the "About" window for the app. Returns the window that is
176
+ # opened.
177
+ #
178
+ # @return [AX::Window]
179
+ def show_about_window
180
+ windows = self.children.select { |x| x.kind_of? AX::Window }
181
+ select_menu_item self.title, /^About /
182
+ wait_for(:window, parent: self) { |window| !windows.include?(window) }
183
+ end
184
+
185
+ ##
186
+ # @note This method assumes that the app has setup the standard
187
+ # CMD+, hotkey to open the pref window
188
+ #
189
+ # Try to open the preferences for the app. Returns the window that
190
+ # is opened.
191
+ #
192
+ # @return [AX::Window]
193
+ def show_preferences_window
194
+ windows = self.children.select { |x| x.kind_of? AX::Window }
195
+ type_string "\\COMMAND+,"
196
+ wait_for(:window, parent: self) { |window| !windows.include?(window) }
197
+ end
198
+
199
+ # @endgroup
200
+
201
+
202
+ ##
203
+ # @todo Include bundle identifier?
204
+ #
205
+ # Override the base class to make sure the pid is included.
206
+ def inspect
207
+ super.sub! />$/, "#{pp_checkbox(:focused)} pid=#{pid}>"
208
+ end
209
+
210
+ ##
211
+ # Find the element in `self` that is present at point given.
212
+ #
213
+ # `nil` will be returned if there was nothing at that point.
214
+ #
215
+ # @param [#to_point]
216
+ # @return [AX::Element,nil]
217
+ def element_at point
218
+ process @ref.element_at point
219
+ end
220
+
221
+ ##
222
+ # Get the bundle identifier for the application.
223
+ #
224
+ # @example
225
+ #
226
+ # safari.bundle_identifier 'com.apple.safari'
227
+ # daylite.bundle_identifier 'com.marketcircle.Daylite'
228
+ #
229
+ # @return [String]
230
+ def bundle_identifier
231
+ @app.bundleIdentifier
232
+ end
233
+
234
+ end
@@ -1,3 +1,5 @@
1
+ require 'ax/element'
2
+
1
3
  ##
2
4
  # A generic push button and the base class for most, but not all,
3
5
  # other buttons, including close buttons and sort buttons, but