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,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