AXElements 0.6.0beta1

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