AXElements 0.6.0beta1

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 (54) hide show
  1. data/.yardopts +20 -0
  2. data/LICENSE.txt +25 -0
  3. data/README.markdown +150 -0
  4. data/Rakefile +109 -0
  5. data/docs/AccessibilityTips.markdown +119 -0
  6. data/docs/Acting.markdown +340 -0
  7. data/docs/Debugging.markdown +326 -0
  8. data/docs/Inspecting.markdown +255 -0
  9. data/docs/KeyboardEvents.markdown +57 -0
  10. data/docs/NewBehaviour.markdown +151 -0
  11. data/docs/Notifications.markdown +271 -0
  12. data/docs/Searching.markdown +250 -0
  13. data/docs/TestingExtensions.markdown +52 -0
  14. data/docs/images/AX.png +0 -0
  15. data/docs/images/all_the_buttons.jpg +0 -0
  16. data/docs/images/ui_hierarchy.dot +34 -0
  17. data/docs/images/ui_hierarchy.png +0 -0
  18. data/ext/key_coder/extconf.rb +6 -0
  19. data/ext/key_coder/key_coder.m +77 -0
  20. data/lib/ax_elements/accessibility/enumerators.rb +104 -0
  21. data/lib/ax_elements/accessibility/language.rb +347 -0
  22. data/lib/ax_elements/accessibility/qualifier.rb +73 -0
  23. data/lib/ax_elements/accessibility.rb +164 -0
  24. data/lib/ax_elements/core.rb +541 -0
  25. data/lib/ax_elements/element.rb +593 -0
  26. data/lib/ax_elements/elements/application.rb +88 -0
  27. data/lib/ax_elements/elements/button.rb +18 -0
  28. data/lib/ax_elements/elements/radio_button.rb +18 -0
  29. data/lib/ax_elements/elements/row.rb +30 -0
  30. data/lib/ax_elements/elements/static_text.rb +17 -0
  31. data/lib/ax_elements/elements/systemwide.rb +46 -0
  32. data/lib/ax_elements/inspector.rb +116 -0
  33. data/lib/ax_elements/macruby_extensions.rb +255 -0
  34. data/lib/ax_elements/notification.rb +37 -0
  35. data/lib/ax_elements/version.rb +9 -0
  36. data/lib/ax_elements.rb +30 -0
  37. data/lib/minitest/ax_elements.rb +19 -0
  38. data/lib/mouse.rb +185 -0
  39. data/lib/rspec/expectations/ax_elements.rb +15 -0
  40. data/test/elements/test_application.rb +72 -0
  41. data/test/elements/test_row.rb +27 -0
  42. data/test/elements/test_systemwide.rb +38 -0
  43. data/test/helper.rb +119 -0
  44. data/test/test_accessibility.rb +127 -0
  45. data/test/test_blankness.rb +26 -0
  46. data/test/test_core.rb +448 -0
  47. data/test/test_element.rb +939 -0
  48. data/test/test_enumerators.rb +81 -0
  49. data/test/test_inspector.rb +121 -0
  50. data/test/test_language.rb +157 -0
  51. data/test/test_macruby_extensions.rb +303 -0
  52. data/test/test_mouse.rb +5 -0
  53. data/test/test_search_semantics.rb +143 -0
  54. metadata +219 -0
@@ -0,0 +1,30 @@
1
+ ##
2
+ # UI Element for the row in a table, outline, etc.
3
+ class AX::Row < AX::Element
4
+
5
+ ##
6
+ # @todo Can this be more efficient? It wraps the columns twice,
7
+ # which is expensive if there are many columns. Needs to be
8
+ # done in such that we preserve search semantics for the
9
+ # filters.
10
+ #
11
+ # Retrieve the child in a row that corresponds to a specific column.
12
+ # You must pass filters here in the same way that you would for a
13
+ # search.
14
+ #
15
+ # @example
16
+ #
17
+ # table.row[5].child_in_column(header: 'Price')
18
+ #
19
+ # @param [Hash]
20
+ # @return [AX::Element]
21
+ def child_in_column filters
22
+ table = self.parent
23
+ column = table.column(filters)
24
+ columns = table.columns
25
+ index = columns.index { |x| x == column }
26
+ return self.children.at(index) if index
27
+ raise AX::Element::SearchFailure.new(self.parent, 'column', filters)
28
+ end
29
+
30
+ end
@@ -0,0 +1,17 @@
1
+ ##
2
+ # Represents text on the screen that cannot directly be changed by
3
+ # a user, usually a label or an instructional text block.
4
+ class AX::StaticText < AX::Element
5
+
6
+ ##
7
+ # Test equality with another object. Equality can be with another
8
+ # {AX::Element} or it can be with a string that matches the value
9
+ # of the static text.
10
+ #
11
+ # @return [Boolean]
12
+ def == other
13
+ return super unless other.kind_of? NSString
14
+ return attribute(:value) == other
15
+ end
16
+
17
+ end
@@ -0,0 +1,46 @@
1
+ require 'singleton'
2
+
3
+ ##
4
+ # Represents the special `SystemWide` accessibility object.
5
+ class AX::SystemWide < AX::Element
6
+ include Singleton
7
+
8
+ ##
9
+ # Overridden since there is only one way to get the element ref.
10
+ def initialize
11
+ ref = AXUIElementCreateSystemWide()
12
+ super ref, AX.attrs_of_element(ref)
13
+ end
14
+
15
+ ##
16
+ # @note With the `SystemWide` class, using {#type_string} will send the
17
+ # events to which ever app has focus.
18
+ #
19
+ # Generate keyboard events by simulating keyboard input.
20
+ def type_string string
21
+ AX.keyboard_action @ref, string
22
+ end
23
+
24
+ ##
25
+ # Overridden to avoid a difficult to understand error message.
26
+ def search *args
27
+ raise NoMethodError, 'AX::SystemWide cannot search'
28
+ end
29
+
30
+ ##
31
+ # Raises an `NoMethodError` instead of (possibly) silently failing to
32
+ # register for a notification.
33
+ #
34
+ # @raise [NoMethodError]
35
+ def on_notification *args
36
+ raise NoMethodError, 'AX::SystemWide cannot register for notifications'
37
+ end
38
+
39
+ end
40
+
41
+
42
+ ##
43
+ # Singleton instance of the SystemWide element
44
+ #
45
+ # @return [AX::SystemWide]
46
+ AX::SYSTEM = AX::SystemWide.instance
@@ -0,0 +1,116 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ ##
4
+ # Convenience methods to use when building an `#inspect` method for
5
+ # {AX::Element} and its descendants.
6
+ #
7
+ # The module only expects three methods in order to operate:
8
+ #
9
+ # - `#attributes` returns a list of available attributes
10
+ # - `#attribute` returns the value of a given attribute
11
+ # - `#size_of` returns the size for an attribute
12
+ #
13
+ module Accessibility::PPInspector
14
+
15
+
16
+ protected
17
+
18
+ ##
19
+ # Added for backwards compatability with Snow Leopard.
20
+ #
21
+ # @return [String]
22
+ KAXIdentifierAttribute = 'AXIdentifier'.freeze
23
+
24
+ ##
25
+ # @todo I feel a bit bad about having such a large method that has
26
+ # some inefficiencies.
27
+ #
28
+ # Create an identifier for `self` using various attributes that should
29
+ # make it very easy to identify the element.
30
+ #
31
+ # @return [String]
32
+ def pp_identifier
33
+ # use or lack of use of #inspect is intentional for visual effect
34
+
35
+ if attributes.include? KAXValueAttribute
36
+ val = attribute :value
37
+ if val.kind_of? NSString
38
+ return " #{val.inspect}" unless val.empty?
39
+ else
40
+ return " value=#{val.inspect}"
41
+ end
42
+ end
43
+
44
+ if attributes.include? KAXTitleAttribute
45
+ val = attribute(:title)
46
+ return " #{val.inspect}" if val && !val.empty?
47
+ end
48
+
49
+ if attributes.include? KAXTitleUIElementAttribute
50
+ val = attribute :title_ui_element
51
+ return BUFFER + val.inspect if val
52
+ end
53
+
54
+ if attributes.include? KAXDescriptionAttribute
55
+ val = attribute(:description).to_s
56
+ return BUFFER + val unless val.empty?
57
+ end
58
+
59
+ if attributes.include? KAXIdentifierAttribute
60
+ return " id=#{attribute(:identifier)}"
61
+ end
62
+
63
+ # @todo should we have other fallbacks?
64
+ return ::EMPTY_STRING
65
+ end
66
+
67
+ ##
68
+ # Create a string that succinctly encodes the screen coordinates
69
+ # of `self`.
70
+ #
71
+ # @return [String]
72
+ def pp_position
73
+ position = attribute :position
74
+ " (#{position.x}, #{position.y})"
75
+ end
76
+
77
+ ##
78
+ # Create a string that nicely presents the number of children
79
+ # that `self` has.
80
+ #
81
+ # @return [String]
82
+ def pp_children
83
+ child_count = size_of :children
84
+ if child_count > 1
85
+ " #{child_count} children"
86
+ elsif child_count == 1
87
+ ' 1 child'
88
+ else # there are some odd edge cases
89
+ ::EMPTY_STRING
90
+ end
91
+ end
92
+
93
+ ##
94
+ # Create a string that looks like a labeled check box. The label
95
+ # is the given attribute, and the check box value will be
96
+ # determined by the value of the attribute.
97
+ #
98
+ # @param [Symbol]
99
+ # @return [String]
100
+ def pp_checkbox attr
101
+ " #{attr}[#{attribute(attr) ? '✔' : '✘'}]"
102
+ end
103
+
104
+
105
+ private
106
+
107
+ ##
108
+ # @private
109
+ #
110
+ # A string with a single space, used as a buffer. This is a
111
+ # performance hack.
112
+ #
113
+ # @return [String]
114
+ BUFFER = ' '.freeze
115
+
116
+ end
@@ -0,0 +1,255 @@
1
+ require 'active_support/inflector'
2
+
3
+ ##
4
+ # Extensions to `NSArray`.
5
+ class NSArray
6
+ ##
7
+ # Equivalent to `#[1]`
8
+ def second
9
+ at(1)
10
+ end
11
+
12
+ ##
13
+ # Equivalent to `#[2]`
14
+ def third
15
+ at(2)
16
+ end
17
+
18
+ ##
19
+ # Create a `CGPoint` from the first two elements in the array.
20
+ #
21
+ # @return [CGPoint]
22
+ def to_point
23
+ CGPoint.new(first, second)
24
+ end
25
+
26
+ ##
27
+ # Create a `CGSize` from the first two elements in the array.
28
+ #
29
+ # @return [CGSize]
30
+ def to_size
31
+ CGSize.new(first, second)
32
+ end
33
+
34
+ ##
35
+ # Create a `CGRect` from the first four elements in the array.
36
+ #
37
+ # @return [CGRect]
38
+ def to_rect
39
+ CGRectMake(*self[0,4])
40
+ end
41
+
42
+ ##
43
+ # @method blank?
44
+ #
45
+ # Borrowed from ActiveSupport. Too bad this docstring isn't being
46
+ # picked up by YARD.
47
+ alias_method :blank?, :empty?
48
+
49
+ alias_method :ax_array_method_missing, :method_missing
50
+ ##
51
+ # @todo We should really rethink what this does in terms of the
52
+ # semantics of the language. For instance, when we say something
53
+ # like "outline.rows.text_field" did we want the text field for
54
+ # each row or did we want a row that has a text field or did we
55
+ # want the first text field that is a child of one of the rows?
56
+ #
57
+ # If the array contains {AX::Element} objects and the method name
58
+ # belongs to an attribute then the method will be mapped
59
+ # across the array. In this case, you can artificially pluralize
60
+ # the attribute name and the lookup will singularize the method name
61
+ # for you.
62
+ #
63
+ # You also have to be careful in cases where the array contains
64
+ # various types of {AX::Element} objects that may not all respond to
65
+ # the same attribute.
66
+ def method_missing method, *args
67
+ if first.kind_of? AX::Element
68
+ return map(&method) if first.respond_to?(method)
69
+ return map(&singularized_method_name(method))
70
+ end
71
+ ax_array_method_missing method, *args
72
+ end
73
+
74
+
75
+ private
76
+
77
+ ##
78
+ # Takes a method name and singularizes it, including the case where
79
+ # the method name is a predicate.
80
+ #
81
+ # @param [Symbol] method
82
+ # @return [Symbol]
83
+ def singularized_method_name method
84
+ (method.predicate? ? method[0..-2] : method).singularize.to_sym
85
+ end
86
+ end
87
+
88
+
89
+ ##
90
+ # @private
91
+ #
92
+ # An empty string, performance hack.
93
+ #
94
+ # @return [String]
95
+ EMPTY_STRING = ''.freeze
96
+
97
+ ##
98
+ # Extensions to `NSString`.
99
+ class NSString
100
+ ##
101
+ # Used to test a symbol/string representing a method name.
102
+ # Returns `true` if the string ends with a '?".
103
+ def predicate?
104
+ self[-1] == '?'
105
+ end
106
+
107
+ ##
108
+ # Force the #singularize method to be defined on NSString objects,
109
+ # and therefore on Symbol objects...at least until that bug gets
110
+ # fixed.
111
+ #
112
+ # @return [String]
113
+ def singularize
114
+ ActiveSupport::Inflector.singularize(self)
115
+ end
116
+
117
+ ##
118
+ # Returns the upper camel case version of the string. The string
119
+ # is assumed to be in `snake_case`, but still works on a string that
120
+ # is already in camel case.
121
+ #
122
+ # I have this method update the string in-place as it is a fairly hot
123
+ # method and should perform well; by running in-place we save an
124
+ # allocation (which is slow on MacRuby right now).
125
+ #
126
+ # Returns `nil` the string was empty.
127
+ #
128
+ # @return [String,nil] returns `self`
129
+ def camelize
130
+ gsub /(?:^|_)(.)/ do $1.upcase! end
131
+ end
132
+ end
133
+
134
+
135
+ ##
136
+ # Extensions to `CGPoint`.
137
+ class CGPoint
138
+ ##
139
+ # Get the center point in a rectangle.
140
+ #
141
+ # @param [CGRect] rect
142
+ # @return [CGPoint]
143
+ def self.center_of_rect rect
144
+ rect.origin.center rect.size
145
+ end
146
+
147
+ ##
148
+ # Find the center of a rectangle, treating `self` as the origin and
149
+ # the given `size` as the size of the rectangle.
150
+ #
151
+ # @param [CGSize] size
152
+ # @return [CGPoint]
153
+ def center size
154
+ x = self.x + (size.width / 2.0)
155
+ y = self.y + (size.height / 2.0)
156
+ CGPoint.new(x, y)
157
+ end
158
+
159
+ ##
160
+ # @note This method does not show up in the documentation with
161
+ # YARD 0.7.x
162
+ #
163
+ # @return [CGPoint]
164
+ alias_method :to_point, :self
165
+
166
+ @ax_value = KAXValueCGPointType
167
+ end
168
+
169
+
170
+ ##
171
+ # Extensions to `Boxed` objects.
172
+ class Boxed
173
+ class << self
174
+ ##
175
+ # The `AXValue` constant for the struct type. Not all structs
176
+ # have a value.
177
+ #
178
+ # @return [AXValueType]
179
+ attr_reader :ax_value
180
+ end
181
+
182
+ ##
183
+ # Create an `AXValue` from the `Boxed` instance. This will only
184
+ # work if for a few boxed types, check the AXAPI documentation.
185
+ #
186
+ # @return [AXValueRef]
187
+ def to_axvalue
188
+ klass = self.class
189
+ ptr = Pointer.new klass.type
190
+ ptr.assign self
191
+ AXValueCreate(klass.ax_value, ptr)
192
+ end
193
+ end
194
+
195
+
196
+ ##
197
+ # Extensions to `CGSize`.
198
+ class CGSize
199
+ @ax_value = KAXValueCGSizeType
200
+ end
201
+
202
+
203
+ ##
204
+ # Extensions to `CGRect`.
205
+ class CGRect
206
+ @ax_value = KAXValueCGRectType
207
+ end
208
+
209
+
210
+ ##
211
+ # Extensions to `CFRange`.
212
+ class CFRange
213
+ @ax_value = KAXValueCFRangeType
214
+ end
215
+
216
+
217
+ ##
218
+ # Extensions to `NilClass`.
219
+ class NilClass
220
+ ##
221
+ # Borrowed from Active Support.
222
+ def blank?
223
+ true
224
+ end
225
+ end
226
+
227
+
228
+ # ##
229
+ # # Correct a problem with ArgumentError not providing a proper backtrace.
230
+ # class ArgumentError
231
+ # alias_method :original_message, :message
232
+ # def message
233
+ # "#{original_message}\n\t#{backtrace.join("\n\t")}"
234
+ # end
235
+ # end
236
+
237
+
238
+ # ##
239
+ # # Correct a problem with NameError not providing a proper backtrace.
240
+ # class NameError
241
+ # alias_method :original_message, :message
242
+ # def message
243
+ # "#{original_message}\n\t#{backtrace.join("\n\t")}"
244
+ # end
245
+ # end
246
+
247
+
248
+ # ##
249
+ # # Correct a problem with NoMethodError not providing a proper backtrace.
250
+ # class NoMethodError
251
+ # alias_method :original_message, :message
252
+ # def message
253
+ # "#{original_message}\n\t#{backtrace.join("\n\t")}"
254
+ # end
255
+ # end
@@ -0,0 +1,37 @@
1
+ class Accessibility::Notification
2
+
3
+ attr_reader :observer
4
+ attr_reader :element
5
+ attr_reader :string
6
+
7
+ def callback observer, element, notif, refcon
8
+ end
9
+
10
+ def initialize element, string
11
+ @element = element
12
+ @string = string
13
+ end
14
+
15
+ def register
16
+ code = AXObserverAddNotification(observer, element, string, nil)
17
+ log_error element, code unless code.zero?
18
+ end
19
+
20
+ def unregister
21
+ case code = AXObserverRemoveNotification(observer, element, string)
22
+ when KAXErrorNotificationNotRegistered
23
+ Accessibility.log.warn "Notif no longer registered: (#{ref}:#{notif})"
24
+ when KAXErrorIllegalArgument
25
+ raise ArgumentError, "Notif not unregistered (#{ref}:#{notif})"
26
+ else
27
+ log_error element, code unless code.zero?
28
+ end
29
+ end
30
+
31
+ def == other
32
+ observer == other.observer &&
33
+ element == other.element &&
34
+ string == other.string
35
+ end
36
+
37
+ end
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Accessibility
4
+ # @return [String]
5
+ VERSION = '0.6.0beta1'
6
+
7
+ # @return [String]
8
+ CODE_NAME = 'Luxray'
9
+ end
@@ -0,0 +1,30 @@
1
+ framework 'Cocoa'
2
+
3
+ # check that the Accessibility APIs are enabled and are available to MacRuby
4
+ begin
5
+ unless AXAPIEnabled()
6
+ raise RuntimeError, <<-EOS
7
+ Universal Access is disabled on this machine. Please enable it in the System Preferences.
8
+ EOS
9
+ end
10
+ rescue NoMethodError
11
+ raise NotImplementedError, <<-EOS
12
+ You need to install the latest BridgeSupport preview for AXElements to work.
13
+ EOS
14
+ end
15
+
16
+ require 'ax_elements/macruby_extensions'
17
+ require 'ax_elements/version'
18
+ require 'ax_elements/core'
19
+ require 'ax_elements/element'
20
+
21
+ require 'ax_elements/accessibility/language'
22
+
23
+ # Mix the language methods in to the TopLevel
24
+ include Accessibility::Language
25
+
26
+ ##
27
+ # The Mac OS X dock application.
28
+ #
29
+ # @return [AX::Application]
30
+ AX::DOCK = Accessibility.application_with_bundle_identifier 'com.apple.dock'
@@ -0,0 +1,19 @@
1
+ ##
2
+ #
3
+ class MiniTest::Unit::TestCase
4
+ # @yield
5
+ def assert_exists
6
+ begin
7
+ yield.blank?
8
+ # blank? if nothing found, kind_of Element otherwise
9
+ rescue AX::Element::SearchFailure => e
10
+ # problem somewhere in the path
11
+ end
12
+ assert_respond_to
13
+ end
14
+
15
+ def refute_exists
16
+ end
17
+ end
18
+
19
+ # @todo assertions for minitest/spec