accessibility_core 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/.yardopts +9 -0
  2. data/History.markdown +36 -0
  3. data/README.markdown +66 -0
  4. data/Rakefile +72 -0
  5. data/ext/accessibility/bridge/ext/accessibility/bridge/bridge.c +490 -0
  6. data/ext/accessibility/bridge/ext/accessibility/bridge/extconf.rb +22 -0
  7. data/ext/accessibility/bridge/lib/accessibility/bridge.rb +33 -0
  8. data/ext/accessibility/bridge/lib/accessibility/bridge/common.rb +57 -0
  9. data/ext/accessibility/bridge/lib/accessibility/bridge/macruby.rb +185 -0
  10. data/ext/accessibility/bridge/lib/accessibility/bridge/mri.rb +121 -0
  11. data/ext/accessibility/bridge/lib/accessibility/bridge/version.rb +6 -0
  12. data/ext/accessibility/bridge/test/array_test.rb +31 -0
  13. data/ext/accessibility/bridge/test/boxed_test.rb +23 -0
  14. data/ext/accessibility/bridge/test/cfrange_test.rb +21 -0
  15. data/ext/accessibility/bridge/test/cgpoint_test.rb +54 -0
  16. data/ext/accessibility/bridge/test/cgrect_test.rb +60 -0
  17. data/ext/accessibility/bridge/test/cgsize_test.rb +54 -0
  18. data/ext/accessibility/bridge/test/helper.rb +19 -0
  19. data/ext/accessibility/bridge/test/nsstring_test.rb +22 -0
  20. data/ext/accessibility/bridge/test/nsurl_test.rb +17 -0
  21. data/ext/accessibility/bridge/test/object_test.rb +19 -0
  22. data/ext/accessibility/bridge/test/range_test.rb +16 -0
  23. data/ext/accessibility/bridge/test/string_test.rb +35 -0
  24. data/ext/accessibility/bridge/test/uri_test.rb +15 -0
  25. data/ext/accessibility/core/bridge.h +1 -0
  26. data/ext/accessibility/core/core.c +705 -0
  27. data/ext/accessibility/core/extconf.rb +22 -0
  28. data/ext/accessibility/highlighter/extconf.rb +22 -0
  29. data/ext/accessibility/highlighter/highlighter.c +7 -0
  30. data/lib/accessibility/core.rb +12 -0
  31. data/lib/accessibility/core/core_ext/common.rb +57 -0
  32. data/lib/accessibility/core/core_ext/macruby.rb +140 -0
  33. data/lib/accessibility/core/core_ext/mri.rb +121 -0
  34. data/lib/accessibility/core/macruby.rb +858 -0
  35. data/lib/accessibility/core/version.rb +6 -0
  36. data/test/helper.rb +48 -0
  37. metadata +158 -0
@@ -0,0 +1,22 @@
1
+ require 'mkmf'
2
+
3
+ $CFLAGS << ' -std=c99 -Wall -Werror -pedantic -ObjC'
4
+ $LIBS << ' -framework CoreFoundation -framework ApplicationServices -framework Cocoa'
5
+ $LIBS << ' -framework CoreGraphics' unless `sw_vers -productVersion`.to_f == 10.7
6
+
7
+ if RUBY_ENGINE == 'macruby'
8
+ $CFLAGS << ' -fobjc-gc'
9
+ else
10
+ unless RbConfig::CONFIG["CC"].match /clang/
11
+ clang = `which clang`.chomp
12
+ if clang.empty?
13
+ raise "Clang not installed. Cannot build C extension"
14
+ else
15
+ RbConfig::MAKEFILE_CONFIG["CC"] = clang
16
+ RbConfig::MAKEFILE_CONFIG["CXX"] = clang
17
+ end
18
+ end
19
+ $CFLAGS << ' -DNOT_MACRUBY'
20
+ end
21
+
22
+ create_makefile 'accessibility/core'
@@ -0,0 +1,22 @@
1
+ require 'mkmf'
2
+
3
+ $CFLAGS << ' -std=c99 -Wall -Werror -pedantic -ObjC'
4
+ $LIBS << ' -framework CoreFoundation -framework Cocoa'
5
+ $LIBS << ' -framework CoreGraphics' unless `sw_vers -productVersion`.to_f == 10.7
6
+
7
+ if RUBY_ENGINE == 'macruby'
8
+ $CFLAGS << ' -fobjc-gc'
9
+ else
10
+ unless RbConfig::CONFIG["CC"].match /clang/
11
+ clang = `which clang`.chomp
12
+ if clang.empty?
13
+ raise "Clang not installed. Cannot build C extension"
14
+ else
15
+ RbConfig::MAKEFILE_CONFIG["CC"] = clang
16
+ RbConfig::MAKEFILE_CONFIG["CXX"] = clang
17
+ end
18
+ end
19
+ $CFLAGS << ' -DNOT_MACRUBY'
20
+ end
21
+
22
+ create_makefile 'accessibility/highlighter'
@@ -0,0 +1,7 @@
1
+ #include "ruby.h"
2
+
3
+ void
4
+ Init_highlighter()
5
+ {
6
+ }
7
+
@@ -0,0 +1,12 @@
1
+ # gems
2
+ require 'accessibility/bridge'
3
+
4
+ # internal deps
5
+ require 'accessibility/core/version'
6
+
7
+ if on_macruby?
8
+ require 'accessibility/core/macruby'
9
+ else
10
+ require 'accessibility/core/core.bundle'
11
+ end
12
+
@@ -0,0 +1,57 @@
1
+ class CGPoint
2
+ ##
3
+ # Returns the receiver, since the receiver is already a {CGPoint}
4
+ #
5
+ # @return [CGPoint]
6
+ def to_point
7
+ self
8
+ end
9
+ end
10
+
11
+ class CGSize
12
+ ##
13
+ # Returns the receiver, since the receiver is already a {CGSize}
14
+ #
15
+ # @return [CGSize]
16
+ def to_size
17
+ self
18
+ end
19
+ end
20
+
21
+ class CGRect
22
+ ##
23
+ # Returns the receiver, since the receiver is already a {CGRect}
24
+ #
25
+ # @return [CGRect]
26
+ def to_rect
27
+ self
28
+ end
29
+ end
30
+
31
+ ##
32
+ # accessibility-core extensions to `Array`
33
+ class Array
34
+ ##
35
+ # Coerce the first two elements of the receiver into a {CGPoint}
36
+ #
37
+ # @return [CGPoint]
38
+ def to_point
39
+ CGPoint.new self[0], self[1]
40
+ end
41
+
42
+ ##
43
+ # Coerce the first two elements of the receiver into a {CGSize}
44
+ #
45
+ # @return [CGSize]
46
+ def to_size
47
+ CGSize.new self[0], self[1]
48
+ end
49
+
50
+ ##
51
+ # Coerce the first four elements of the receiver into a {CGRect}
52
+ #
53
+ # @return [CGRect]
54
+ def to_rect
55
+ CGRect.new CGPoint.new(self[0], self[1]), CGSize.new(self[2], self[3])
56
+ end
57
+ end
@@ -0,0 +1,140 @@
1
+ require 'accessibility/core/core_ext/common'
2
+
3
+ ##
4
+ # accessibility-core extensions for `NSURL`
5
+ class NSURL
6
+ ##
7
+ # Return the reciver, for the receiver is already a URL object
8
+ #
9
+ # @return [NSURL]
10
+ def to_url
11
+ self
12
+ end
13
+
14
+ # because printing is easier this way
15
+ alias_method :to_s, :inspect
16
+ end
17
+
18
+ ##
19
+ # accessibility-core extensions for `NSString`
20
+ class NSString
21
+ ##
22
+ # Create an NSURL using the receiver as the initialization string
23
+ #
24
+ # If the receiver is not a valid URL then `nil` will be returned.
25
+ #
26
+ # This exists because of
27
+ # [rdar://11207662](http://openradar.appspot.com/11207662).
28
+ #
29
+ # @return [NSURL,nil]
30
+ def to_url
31
+ NSURL.URLWithString self
32
+ end
33
+ end
34
+
35
+ ##
36
+ # `accessibility-core` extensions for `NSObject`
37
+ class NSObject
38
+ ##
39
+ # Return an object safe for passing to AXAPI
40
+ def to_ax
41
+ self
42
+ end
43
+
44
+ ##
45
+ # Return a usable object from an AXAPI pointer
46
+ def to_ruby
47
+ self
48
+ end
49
+ end
50
+
51
+ ##
52
+ # `accessibility-core` extensions for `CFRange`
53
+ class CFRange
54
+ ##
55
+ # Convert the {CFRange} to a Ruby {Range} object
56
+ #
57
+ # @return [Range]
58
+ def to_ruby
59
+ Range.new location, (location + length - 1)
60
+ end
61
+ end
62
+
63
+ ##
64
+ # `accessibility-core` extensions for `Range`
65
+ class Range
66
+ # @return [AXValueRef]
67
+ def to_ax
68
+ raise ArgumentError, "can't convert negative index" if last < 0 || first < 0
69
+ length = if exclude_end?
70
+ last - first
71
+ else
72
+ last - first + 1
73
+ end
74
+ CFRange.new(first, length).to_ax
75
+ end
76
+ end
77
+
78
+ ##
79
+ # AXElements extensions to the `Boxed` class
80
+ #
81
+ # The `Boxed` class is simply an abstract base class for structs that
82
+ # MacRuby can use via bridge support.
83
+ class Boxed
84
+ ##
85
+ # Returns the number that AXAPI uses in order to know how to wrap
86
+ # a struct.
87
+ #
88
+ # @return [Number]
89
+ def self.ax_value
90
+ raise NotImplementedError, "#{inspect}:#{self.class} cannot be wrapped"
91
+ end
92
+
93
+ ##
94
+ # Create an `AXValueRef` from the `Boxed` instance. This will only
95
+ # work if for the most common boxed types, you will need to check
96
+ # the AXAPI documentation for an up to date list.
97
+ #
98
+ # @example
99
+ #
100
+ # CGPoint.new(12, 34).to_ax # => #<AXValueRef:0x455678e2>
101
+ # CGSize.new(56, 78).to_ax # => #<AXValueRef:0x555678e2>
102
+ #
103
+ # @return [AXValueRef]
104
+ def to_ax
105
+ klass = self.class
106
+ ptr = Pointer.new klass.type
107
+ ptr.assign self
108
+ AXValueCreate(klass.ax_value, ptr)
109
+ end
110
+ end
111
+
112
+ # `accessibility-core` extensions for `CFRange`'s metaclass
113
+ class << CFRange
114
+ # (see Boxed.ax_value)
115
+ def ax_value; KAXValueCFRangeType end
116
+ end
117
+ # `accessibility-core` extensions for `CGSize`'s metaclass
118
+ class << CGSize
119
+ # (see Boxed.ax_value)
120
+ def ax_value; KAXValueCGSizeType end
121
+ end
122
+ # `accessibility-core` extensions for `CGRect`'s metaclass
123
+ class << CGRect
124
+ # (see Boxed.ax_value)
125
+ def ax_value; KAXValueCGRectType end
126
+ end
127
+ # `accessibility-core` extensions for `CGPoint`'s metaclass
128
+ class << CGPoint
129
+ # (see Boxed.ax_value)
130
+ def ax_value; KAXValueCGPointType end
131
+ end
132
+
133
+ ##
134
+ # `accessibility-core` extensions to `NSArray`
135
+ class NSArray
136
+ # @return [Array]
137
+ def to_ruby
138
+ map do |obj| obj.to_ruby end
139
+ end
140
+ end
@@ -0,0 +1,121 @@
1
+ ##
2
+ # A structure that contains a point in a two-dimensional coordinate system
3
+ CGPoint = Struct.new(:x, :y) do
4
+
5
+ # @param x [Number]
6
+ # @param y [Number]
7
+ def initialize x = 0.0, y = 0.0
8
+ super
9
+ end
10
+
11
+ # @!attribute [rw] x
12
+ # The `x` co-ordinate of the screen point
13
+ # @return [Number]
14
+
15
+ # @!attribute [rw] y
16
+ # The `y` co-ordinate of the screen point
17
+ # @return [Number]
18
+
19
+ ##
20
+ # Return a nice string representation of the point
21
+ #
22
+ # Overrides `Object#inspect` to more closely mimic MacRuby `Boxed#inspect`.
23
+ #
24
+ # @return [String]
25
+ def inspect
26
+ "#<CGPoint x=#{self.x.to_f} y=#{self.y.to_f}>"
27
+ end
28
+
29
+ end
30
+
31
+
32
+ ##
33
+ # A structure that contains the size of a rectangle in a 2D co-ordinate system
34
+ CGSize = Struct.new(:width, :height) do
35
+
36
+ # @param width [Number]
37
+ # @param height [Number]
38
+ def initialize width = 0.0, height = 0.0
39
+ super
40
+ end
41
+
42
+ # @!attribute [rw] width
43
+ # The `width` of the box
44
+ # @return [Number]
45
+
46
+ # @!attribute [rw] height
47
+ # The `heighth` of the box
48
+ # @return [Number]
49
+
50
+ ##
51
+ # Return a nice string representation of the size
52
+ #
53
+ # Overrides `Object#inspect` to more closely mimic MacRuby `Boxed#inspect`.
54
+ #
55
+ # @return [String]
56
+ def inspect
57
+ "#<CGSize width=#{self.width.to_f} height=#{self.height.to_f}>"
58
+ end
59
+
60
+ end
61
+
62
+
63
+ ##
64
+ # Complete definition of a rectangle in a 2D coordinate system
65
+ CGRect = Struct.new(:origin, :size) do
66
+
67
+ # @param origin [CGPoint,#to_point]
68
+ # @param size [CGSize,#to_size]
69
+ def initialize origin = CGPoint.new, size = CGSize.new
70
+ super(origin.to_point, size.to_size)
71
+ end
72
+
73
+ # @!attribute [rw] origin
74
+ # The `origin` point
75
+ # @return [CGPoint,#to_point]
76
+
77
+ # @!attribute [rw] size
78
+ # The `size` of the rectangle
79
+ # @return [CGSize,#to_size]
80
+
81
+ ##
82
+ # Return a nice string representation of the rectangle
83
+ #
84
+ # Overrides `Object#inspect` to more closely mimic MacRuby `Boxed#inspect`.
85
+ #
86
+ # @return [String]
87
+ def inspect
88
+ "#<CGRect origin=#{self.origin.inspect} size=#{self.size.inspect}>"
89
+ end
90
+
91
+ end
92
+
93
+
94
+ require 'uri'
95
+
96
+ ##
97
+ # `accessibility-core` extensions to the `URI` family of classes
98
+ class URI::Generic
99
+ ##
100
+ # Returns the receiver (since the receiver is already a `URI` object)
101
+ #
102
+ # @return [URI::Generic]
103
+ def to_url
104
+ self
105
+ end
106
+ end
107
+
108
+ ##
109
+ # `accessibility-core` extensions to the `String` class
110
+ class String
111
+ ##
112
+ # Parse the receiver into a `URI` object
113
+ #
114
+ # @return [URI::Generic]
115
+ def to_url
116
+ URI.parse self
117
+ end
118
+ end
119
+
120
+
121
+ require 'accessibility/core/core_ext/common'
@@ -0,0 +1,858 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ framework 'Cocoa'
4
+
5
+ # A workaround that guarantees that `CGPoint` is defined
6
+ MOUNTAIN_LION_APPKIT_VERSION ||= 1187
7
+ if NSAppKitVersionNumber >= MOUNTAIN_LION_APPKIT_VERSION
8
+ framework '/System/Library/Frameworks/CoreGraphics.framework'
9
+ end
10
+
11
+ # check that the Accessibility APIs are enabled and are available to MacRuby
12
+ begin
13
+ unless AXAPIEnabled()
14
+ raise RuntimeError, <<-EOS
15
+ ------------------------------------------------------------------------
16
+ Universal Access is disabled on this machine.
17
+
18
+ Please enable it in the System Preferences.
19
+ ------------------------------------------------------------------------
20
+ EOS
21
+ end
22
+ rescue NoMethodError
23
+ raise NotImplementedError, <<-EOS
24
+ ------------------------------------------------------------------------
25
+ You need to install the latest BridgeSupport preview so that AXElements
26
+ has access to CoreFoundation.
27
+ ------------------------------------------------------------------------
28
+ EOS
29
+ end
30
+
31
+
32
+ require 'accessibility/core/core_ext/macruby'
33
+
34
+
35
+ ##
36
+ # Core abstraction layer that that adds OO to the OS X Accessibility APIs
37
+ #
38
+ # This provides a generic object oriented mixin/class for the low level APIs.
39
+ # A more Ruby-ish wrapper is available through
40
+ # [AXElements](https://github.com/Marketcircle/AXElements).
41
+ #
42
+ # On MacRuby, bridge support turns C structs into "first class" objects. To
43
+ # that end, instead of adding an extra allocation to wrap the object, we will
44
+ # simply add a mixin to add some basic functionality. On MRI, a C extension
45
+ # encapsulates the structures into a class.
46
+ #
47
+ # For both Ruby platforms the interface should be the same. It is a bug if they
48
+ # are different.
49
+ #
50
+ # This module is responsible for handling pointers and dealing with error
51
+ # codes for functions that make use of them. The methods in this class
52
+ # provide a cleaner, more Ruby-ish interface to the low level CoreFoundation
53
+ # functions that compose AXAPI than are natively available.
54
+ #
55
+ # @example
56
+ #
57
+ # element = Accessibility::Element.application_for pid_for_terminal
58
+ # element.attributes # => ["AXRole", "AXChildren", ...]
59
+ # element.size_of "AXChildren" # => 2
60
+ # element.children.first.role # => "AXWindow"
61
+ #
62
+ module Accessibility::Element
63
+
64
+ class << self
65
+
66
+ # @!group Element Hierarchy Entry Points
67
+
68
+ ##
69
+ # Get the application object object for an application given the
70
+ # process identifier (PID) for that application.
71
+ #
72
+ # @example
73
+ #
74
+ # app = Element.application_for 54743 # => #<Accessibility::Element>
75
+ #
76
+ # @param pid [Number]
77
+ # @return [Accessibility::Element]
78
+ def application_for pid
79
+ NSRunLoop.currentRunLoop.runUntilDate Time.now
80
+ if NSRunningApplication.runningApplicationWithProcessIdentifier pid
81
+ AXUIElementCreateApplication(pid)
82
+ else
83
+ raise ArgumentError, 'pid must belong to a running application'
84
+ end
85
+ end
86
+
87
+ ##
88
+ # Create a new reference to the system wide object
89
+ #
90
+ # This is very useful when working with the system wide object as
91
+ # caching the system wide reference does not seem to work.
92
+ #
93
+ # @example
94
+ #
95
+ # system_wide # => #<Accessibility::Element>
96
+ #
97
+ # @return [Accessibility::Element]
98
+ def system_wide
99
+ AXUIElementCreateSystemWide()
100
+ end
101
+
102
+ ##
103
+ # Find the top most element at the given point on the screen
104
+ #
105
+ # This is the same as {Accessibility::Element#element_at} except
106
+ # that the check for the topmost element does not care which app
107
+ # the element belongs to.
108
+ #
109
+ # The coordinates should be specified using the flipped coordinate
110
+ # system (origin is in the top-left, increasing downward and to the right
111
+ # as if reading a book in English).
112
+ #
113
+ # If more than one element is at the position then the z-order of the
114
+ # elements will be used to determine which is "on top".
115
+ #
116
+ # This method will safely return `nil` if there is no UI element at the
117
+ # give point.
118
+ #
119
+ # @example
120
+ #
121
+ # Element.element_at [453, 200] # table
122
+ # Element.element_at CGPoint.new(453, 200) # table
123
+ #
124
+ # @param point [#to_point]
125
+ # @return [Accessibility::Element,nil]
126
+ def element_at point
127
+ system_wide.element_at point
128
+ end
129
+
130
+ # @!endgroup
131
+
132
+
133
+ ##
134
+ # The delay between keyboard events used by {Accessibility::Element#post}
135
+ #
136
+ # The default value is `0.009` (`:normal`), which should be about 50
137
+ # characters per second (down and up are separate events).
138
+ #
139
+ # This is just a magic number from trial and error. Both the repeat
140
+ # interval (NXKeyRepeatInterval) and threshold (NXKeyRepeatThreshold)
141
+ # were tried, but were way too big.
142
+ #
143
+ # @return [Number]
144
+ attr_reader :key_rate
145
+
146
+ ##
147
+ # Set the delay between key events
148
+ #
149
+ # This value is used by {Accessibility::Element#post} to slow down the
150
+ # typing speed so apps do not get overloaded by all the events arriving
151
+ # at the same time.
152
+ #
153
+ # You can pass either an exact value for sleeping (a `Float` or
154
+ # `Fixnum`), or you can use a preset symbol:
155
+ #
156
+ # - `:very_slow`
157
+ # - `:slow`
158
+ # - `:normal`/`:default`
159
+ # - `:fast`
160
+ # - `:zomg`
161
+ #
162
+ # The `:zomg` setting will be too fast in almost all cases, but
163
+ # it is fun to watch.
164
+ #
165
+ # @param [Number,Symbol]
166
+ def self.key_rate= value
167
+ @key_rate = case value
168
+ when :very_slow then 0.9
169
+ when :slow then 0.09
170
+ when :normal, :default then 0.009
171
+ when :fast then 0.0009
172
+ when :zomg then 0.00009
173
+ else value.to_f
174
+ end
175
+ end
176
+ end
177
+ @key_rate = 0.009
178
+
179
+
180
+ # @!group Attributes
181
+
182
+ ##
183
+ # @todo Invalid elements do not always raise an error. This is a bug
184
+ # that should be logged with Apple (but I keep procrastinating).
185
+ #
186
+ # Get the list of attributes for the element
187
+ #
188
+ # As a convention, this method will return an empty array if the
189
+ # backing element is no longer alive.
190
+ #
191
+ # @example
192
+ #
193
+ # button.attributes # => ["AXRole", "AXRoleDescription", ...]
194
+ #
195
+ # @return [Array<String>]
196
+ def attributes
197
+ @attributes ||= (
198
+ ptr = Pointer.new ARRAY
199
+ code = AXUIElementCopyAttributeNames(self, ptr)
200
+
201
+ case code
202
+ when 0 then ptr.value
203
+ when KAXErrorInvalidUIElement then []
204
+ else handle_error code
205
+ end
206
+ )
207
+ end
208
+
209
+ ##
210
+ # Fetch the value for the given attribute from the receiver
211
+ #
212
+ # CoreFoundation wrapped objects will be unwrapped for you, if you expect
213
+ # to get a {CFRange} you will be given a {Range} instead.
214
+ #
215
+ # As a convention, if the backing element is no longer alive then
216
+ # any attribute value will return `nil`. If the attribute is not supported
217
+ # by the element then `nil` will be returned instead. These
218
+ # conventions are debatably necessary, inquire for details.
219
+ #
220
+ # @example
221
+ # window.attribute 'AXTitle' # => "HotCocoa Demo"
222
+ # window.attribute 'AXSize' # => #<CGSize width=10.0 height=88>
223
+ # window.attribute 'AXParent' # => #<Accessibility::Element>
224
+ # window.attribute 'AXHerpDerp' # => nil
225
+ #
226
+ # @param name [String]
227
+ def attribute name
228
+ ptr = Pointer.new :id
229
+ code = AXUIElementCopyAttributeValue(self, name, ptr)
230
+
231
+ case code
232
+ when 0
233
+ ptr.value.to_ruby
234
+ when KAXErrorFailure, KAXErrorNoValue,
235
+ KAXErrorInvalidUIElement, KAXErrorAttributeUnsupported
236
+ nil
237
+ else
238
+ handle_error code, name
239
+ end
240
+ end
241
+
242
+ ##
243
+ # @note It has been observed that some elements may lie with this value.
244
+ # Bugs should be reported to the app developers in those cases.
245
+ # I'm looking at you, Safari!
246
+ #
247
+ # Get the size of the array that would be returned by calling {#attribute}
248
+ #
249
+ # When performance matters, this is much faster than getting the array
250
+ # and asking for the size.
251
+ #
252
+ # If there is a failure or the backing element is no longer alive, this
253
+ # method will return `0`.
254
+ #
255
+ # @example
256
+ #
257
+ # window.size_of 'AXChildren' # => 19
258
+ # table.size_of 'AXRows' # => 100
259
+ #
260
+ # @param name [String]
261
+ # @return [Number]
262
+ def size_of name
263
+ ptr = Pointer.new :long_long
264
+ code = AXUIElementGetAttributeValueCount(self, name, ptr)
265
+
266
+ case code
267
+ when 0
268
+ ptr.value
269
+ when KAXErrorFailure, KAXErrorNoValue, KAXErrorInvalidUIElement
270
+ 0
271
+ else
272
+ handle_error code, name
273
+ end
274
+ end
275
+
276
+ ##
277
+ # Returns whether or not the given attribute is writable on the reciver
278
+ #
279
+ # Often, you will want/need to check writability of an attribute before
280
+ # trying to call {Accessibility::Element#set} for the attribute.
281
+ #
282
+ # In case of internal error, or if the element dies, this method will
283
+ # return `false`.
284
+ #
285
+ # @example
286
+ #
287
+ # window.writable? 'AXSize' # => true
288
+ # window.writable? 'AXTitle' # => false
289
+ #
290
+ # @param name [String]
291
+ def writable? name
292
+ ptr = Pointer.new :bool
293
+ code = AXUIElementIsAttributeSettable(self, name, ptr)
294
+
295
+ case code
296
+ when 0
297
+ ptr.value
298
+ when KAXErrorFailure, KAXErrorNoValue, KAXErrorInvalidUIElement
299
+ false
300
+ else
301
+ handle_error code, name
302
+ end
303
+ end
304
+
305
+ ##
306
+ # Set the given value for the given attribute on the receiver
307
+ #
308
+ # You do not need to worry about wrapping objects first, `Range`
309
+ # objects will also be automatically converted into `CFRange` objects
310
+ # (unless they have a negative index) and then wrapped.
311
+ #
312
+ # This method does not check writability of the attribute you are
313
+ # setting. If you need to check, use {Accessibility::Element#writable?}
314
+ # first to check.
315
+ #
316
+ # Unlike when reading attributes, writing to a dead element, and
317
+ # other error conditions, will raise an exception.
318
+ #
319
+ # @example
320
+ #
321
+ # set 'AXValue', "hi" # => "hi"
322
+ # set 'AXSize', [250,250] # => [250,250]
323
+ # set 'AXVisibleRange', 0..3 # => 0..3
324
+ # set 'AXVisibleRange', 1...4 # => 1..3
325
+ #
326
+ # @param name [String]
327
+ # @param value [Object]
328
+ def set name, value
329
+ code = AXUIElementSetAttributeValue(self, name, value.to_ax)
330
+ if code.zero?
331
+ value
332
+ else
333
+ handle_error code, name, value
334
+ end
335
+ end
336
+
337
+ ##
338
+ # Shortcut for getting the `"AXRole"` attribute
339
+ #
340
+ # The role of an element roughly translates to the class of the object;
341
+ # however this can be thought of as the superclass of the object if the
342
+ # object also has a `"AXSubrole"` attribute.
343
+ #
344
+ # Remember that dead elements may return `nil` for their role.
345
+ #
346
+ # @example
347
+ #
348
+ # window.role # => "AXWindow"
349
+ #
350
+ # @return [String,nil]
351
+ def role
352
+ attribute KAXRoleAttribute
353
+ end
354
+
355
+ ##
356
+ # @note You might get `nil` back as the subrole even if the object claims
357
+ # to have a subrole attribute. AXWebArea objects are known to do this.
358
+ # You need to check. :(
359
+ #
360
+ # Shortcut for getting the `"AXSubrole"`
361
+ #
362
+ # The subrole of an element roughly translates to the class of the object,
363
+ # but only if the object has a subrole. If an object does not have a subrole
364
+ # then the class of the object would be the {#role}.
365
+ #
366
+ # @example
367
+ # window.subrole # => "AXDialog"
368
+ # web_area.subrole # => nil
369
+ #
370
+ # @return [String,nil]
371
+ def subrole
372
+ attribute KAXSubroleAttribute
373
+ end
374
+
375
+ ##
376
+ # Shortcut for getting the `"AXParent"`
377
+ #
378
+ # The "parent" attribute of an element is the general way in which you would
379
+ # navigate upwards through the hierarchy of the views in an app.
380
+ #
381
+ # An element will be returned if the receiver has a parent, otherwise `nil`
382
+ # will be returned. Incorrectly implemented elements may also return `nil`.
383
+ # Usually only something that has a {#role} of `"AXApplication"` will return
384
+ # `nil` since it does not have a parent.
385
+ #
386
+ # @example
387
+ #
388
+ # window.parent # => app
389
+ # app.parent # => nil
390
+ #
391
+ # @return [Accessibility::Element,nil]
392
+ def parent
393
+ ptr = Pointer.new :id
394
+ code = AXUIElementCopyAttributeValue(self, KAXParentAttribute, ptr)
395
+ code.zero? ? ptr.value.to_ruby : nil
396
+ end
397
+
398
+ ##
399
+ # Shortcut for getting the `"AXChildren"`
400
+ #
401
+ # The "children" attribute of an element is the general way in which you would
402
+ # navigate downwards through the hierarchy of the views in an app.
403
+ #
404
+ # An array will always be returned, even if the element is dead or has no
405
+ # children (but the array will be empty in those cases).
406
+ #
407
+ # @example
408
+ #
409
+ # app.children # => [MenuBar, Window, ...]
410
+ #
411
+ # @return [Array<Accessibility::Element>]
412
+ def children
413
+ ptr = Pointer.new :id
414
+ code = AXUIElementCopyAttributeValue(self, KAXChildrenAttribute, ptr)
415
+ code.zero? ? ptr.value.to_ruby : []
416
+ end
417
+
418
+ ##
419
+ # Shortcut for getting the `"AXValue"`
420
+ #
421
+ # @example
422
+ #
423
+ # label.value # => "Mark Rada"
424
+ # slider.value # => 42
425
+ #
426
+ def value
427
+ attribute KAXValueAttribute
428
+ end
429
+
430
+ ##
431
+ # Get the process identifier (PID) of the application of the receiver
432
+ #
433
+ # This method will return `0` if the element is dead or if the receiver
434
+ # is the the system wide element.
435
+ #
436
+ # @example
437
+ #
438
+ # window.pid # => 12345
439
+ # Element.system_wide.pid # => 0
440
+ #
441
+ # @return [Fixnum]
442
+ def pid
443
+ @pid ||= (
444
+ ptr = Pointer.new :int
445
+ code = AXUIElementGetPid(self, ptr)
446
+
447
+ case code
448
+ when 0
449
+ ptr.value
450
+ when KAXErrorInvalidUIElement
451
+ self == Accessibility::Element.system_wide ? 0 : handle_error(code)
452
+ else
453
+ handle_error code
454
+ end
455
+ )
456
+ end
457
+
458
+
459
+ # @!group Parameterized Attributes
460
+
461
+ ##
462
+ # Get the list of parameterized attributes for the element
463
+ #
464
+ # Similar to {#attributes}, this method will also return an empty
465
+ # array if the element is dead.
466
+ #
467
+ # Most elements do not have parameterized attributes, but the ones
468
+ # that do, have many.
469
+ #
470
+ # @example
471
+ #
472
+ # text_area.parameterized_attributes # => ["AXStringForRange", ...]
473
+ # app.parameterized_attributes # => []
474
+ #
475
+ # @return [Array<String>]
476
+ def parameterized_attributes
477
+ @parameterized_attributes ||= (
478
+ ptr = Pointer.new ARRAY
479
+ code = AXUIElementCopyParameterizedAttributeNames(self, ptr)
480
+
481
+ case code
482
+ when 0 then ptr.value
483
+ when KAXErrorNoValue, KAXErrorInvalidUIElement then []
484
+ else handle_error code
485
+ end
486
+ )
487
+ end
488
+
489
+ ##
490
+ # Fetch the given pramaeterized attribute value for the given parameter
491
+ #
492
+ # Low level objects, such as {Accessibility::Element} and {Boxed} objects,
493
+ # will be unwrapped for you automatically and {CFRange} objects will be
494
+ # turned into {Range} objects. Similarly, you do not need to worry about
495
+ # wrapping the parameter as that will be done for you (except for {Range}
496
+ # objects that use a negative index).
497
+ #
498
+ # As a convention, if the backing element is no longer alive, or the
499
+ # attribute does not exist, or a system failure occurs then you will
500
+ # receive `nil`.
501
+ #
502
+ # @example
503
+ #
504
+ # parameterized_attribute KAXStringForRangeParameterizedAttribute, 1..10
505
+ # # => "ello, worl"
506
+ #
507
+ # @param name [String]
508
+ # @param param [Object]
509
+ def parameterized_attribute name, param
510
+ ptr = Pointer.new :id
511
+ code = AXUIElementCopyParameterizedAttributeValue(self, name, param.to_ax, ptr)
512
+
513
+ case code
514
+ when 0
515
+ ptr.value.to_ruby
516
+ when KAXErrorFailure, KAXErrorNoValue, KAXErrorInvalidUIElement
517
+ nil
518
+ else
519
+ handle_error code, name, param
520
+ end
521
+ end
522
+
523
+
524
+ # @!group Actions
525
+
526
+ ##
527
+ # Get the list of actions that the element can perform
528
+ #
529
+ # If an element does not have actions, then an empty list will be
530
+ # returned. Dead elements will also return an empty array.
531
+ #
532
+ # @example
533
+ #
534
+ # button.actions # => ["AXPress"]
535
+ #
536
+ # @return [Array<String>]
537
+ def actions
538
+ @actions ||= (
539
+ ptr = Pointer.new ARRAY
540
+ code = AXUIElementCopyActionNames(self, ptr)
541
+
542
+ case code
543
+ when 0 then ptr.value
544
+ when KAXErrorInvalidUIElement then []
545
+ else handle_error code
546
+ end
547
+ )
548
+ end
549
+
550
+ ##
551
+ # Ask the receiver to perform the given action
552
+ #
553
+ # This method will always return true or raises an exception. Actions
554
+ # should never fail, but there are some extreme edge cases (e.g. out
555
+ # of memory, etc.).
556
+ #
557
+ # Unlike when reading attributes, performing an action on a dead element
558
+ # will raise an exception.
559
+ #
560
+ # @example
561
+ #
562
+ # perform KAXPressAction # => true
563
+ #
564
+ # @param action [String]
565
+ # @return [Boolean]
566
+ def perform action
567
+ code = AXUIElementPerformAction(self, action)
568
+ if code.zero?
569
+ true
570
+ else
571
+ handle_error code, action
572
+ end
573
+ end
574
+
575
+ ##
576
+ # Post the list of given keyboard events to the receiver
577
+ #
578
+ # This only applies if the given element is an application object or
579
+ # the system wide object. The focused element will receive the events.
580
+ #
581
+ # Events could be generated from a string using output from the
582
+ # `accessibility_keyboard` gem's `Accessibility::String#keyboard_events_for`
583
+ # method.
584
+ #
585
+ # Events are number/boolean tuples, where the number is a keycode
586
+ # and the boolean is the keypress state (true is keydown, false is
587
+ # keyup).
588
+ #
589
+ # You can learn more about keyboard events from the
590
+ # [Keyboard Events documentation](http://github.com/Marketcircle/AXElements/wiki/Keyboarding).
591
+ #
592
+ # @example
593
+ #
594
+ # include Accessibility::String
595
+ # events = keyboard_events_for "Hello, world!\n"
596
+ # app.post events
597
+ #
598
+ # @param events [Array<Array(Number,Boolean)>]
599
+ # @return [self]
600
+ def post events
601
+ events.each do |event|
602
+ code = AXUIElementPostKeyboardEvent(self, 0, *event)
603
+ handle_error code unless code.zero?
604
+ sleep Accessibility::Element.key_rate
605
+ end
606
+ sleep 0.1 # in many cases, UI is not done updating right away
607
+ self
608
+ end
609
+
610
+
611
+ # @!group Misc.
612
+
613
+ ##
614
+ # Return whether or not the receiver is "dead"
615
+ #
616
+ # A dead element is one that is no longer in the app's view
617
+ # hierarchy. This is not the same as visibility; an element that is
618
+ # invalid will not be visible, but an invisible element might still
619
+ # be valid (it depends on the clients implementation of the API).
620
+ def invalid?
621
+ AXUIElementCopyAttributeValue(self, KAXRoleAttribute, Pointer.new(:id)) ==
622
+ KAXErrorInvalidUIElement
623
+ end
624
+
625
+ ##
626
+ # Change the timeout value for the element
627
+ #
628
+ # The timeout value is mostly effective for apps that are slow to respond to
629
+ # accessibility queries, or if you intend to make a large query (such as thousands
630
+ # of rows in a table).
631
+ #
632
+ # If you change the timeout on the system wide object, it affets all timeouts.
633
+ #
634
+ # Setting the global timeout to `0` seconds will reset the timeout value
635
+ # to the system default. The system default timeout value is `6 seconds`
636
+ # as of the writing of this documentation, but Apple has not publicly
637
+ # documented this (we had to ask in person at WWDC).
638
+ #
639
+ # @param seconds [Number]
640
+ # @return [Number]
641
+ def set_timeout_to seconds
642
+ case code = AXUIElementSetMessagingTimeout(self, seconds)
643
+ when 0 then seconds
644
+ else handle_error code, seconds
645
+ end
646
+ end
647
+
648
+ ##
649
+ # Returns the application reference (toplevel element) for the receiver
650
+ #
651
+ # @return [Accessibility::Element]
652
+ def application
653
+ Accessibility::Element.application_for pid
654
+ end
655
+
656
+
657
+ # @!group Element Hierarchy Entry Points
658
+
659
+ ##
660
+ # Find the topmost element at the given point for the receiver's app
661
+ #
662
+ # If the receiver is the system wide object then the return is the
663
+ # topmost element regardless of application.
664
+ #
665
+ # The coordinates should be specified using the flipped coordinate
666
+ # system (origin is in the top-left, increasing downward and to the right
667
+ # as if reading a book in English).
668
+ #
669
+ # If more than one element is at the position then the z-order of the
670
+ # elements will be used to determine which is "on top".
671
+ #
672
+ # This method will safely return `nil` if there is no UI element at the
673
+ # give point.
674
+ #
675
+ # @example
676
+ #
677
+ # Element.system_wide.element_at [453, 200] # table
678
+ # app.element_at CGPoint.new(453, 200) # table
679
+ #
680
+ # @param point [#to_point]
681
+ # @return [Accessibility::Element,nil]
682
+ def element_at point
683
+ ptr = Pointer.new ELEMENT
684
+ code = AXUIElementCopyElementAtPosition(self, *point.to_point, ptr)
685
+
686
+ case code
687
+ when 0
688
+ ptr.value.to_ruby
689
+ when KAXErrorNoValue
690
+ nil
691
+ when KAXErrorInvalidUIElement # @todo uhh, why is this here again?
692
+ unless self == Accessibility::Element.system_wide
693
+ Accessibility::Element.element_at point
694
+ end
695
+ else
696
+ handle_error code, point, nil, nil
697
+ end
698
+ end
699
+
700
+
701
+ private
702
+
703
+ # @!group Error Handling
704
+
705
+ ##
706
+ # @private
707
+ #
708
+ # Mapping of `AXError` values to static information on how to handle
709
+ # the error. Used by {handle_error}.
710
+ #
711
+ # @return [Hash{Number=>Array(Symbol,Range)}]
712
+ AXERROR = {
713
+ KAXErrorFailure => [
714
+ RuntimeError,
715
+ lambda { |*args|
716
+ "A system failure occurred with #{args[0].inspect}, stopping to be safe"
717
+ }
718
+ ],
719
+ KAXErrorIllegalArgument => [
720
+ ArgumentError,
721
+ lambda { |*args|
722
+ case args.size
723
+ when 1
724
+ "#{args[0].inspect} is not an Accessibility::Element"
725
+ when 2
726
+ "Either the element #{args[0].inspect} or the attribute/action" +
727
+ "#{args[1].inspect} is not a legal argument"
728
+ when 3
729
+ "You can't get/set #{args[1].inspect} with/to #{args[2].inspect} " +
730
+ "for #{args[0].inspect}"
731
+ when 4
732
+ "The point #{args[1].to_point.inspect} is not a valid point, " +
733
+ "or #{args[0].inspect} is not an Accessibility::Element"
734
+ end
735
+ }
736
+ ],
737
+ KAXErrorInvalidUIElement => [
738
+ ArgumentError,
739
+ lambda { |*args|
740
+ "#{args[0].inspect} is no longer a valid reference"
741
+ }
742
+ ],
743
+ KAXErrorInvalidUIElementObserver => [
744
+ ArgumentError,
745
+ lambda { |*args|
746
+ 'AXElements no longer supports notifications'
747
+ }
748
+ ],
749
+ KAXErrorCannotComplete => [
750
+ RuntimeError,
751
+ lambda { |*args|
752
+ NSRunLoop.currentRunLoop.runUntilDate Time.now # spin the run loop once
753
+ pid = args[0].pid
754
+ app = NSRunningApplication.runningApplicationWithProcessIdentifier pid
755
+ if app
756
+ "An unspecified error occurred using #{args[0].inspect} with AXAPI, maybe a timeout :("
757
+ else
758
+ "Application for pid=#{pid} is no longer running. Maybe it crashed?"
759
+ end
760
+ }
761
+ ],
762
+ KAXErrorAttributeUnsupported => [
763
+ ArgumentError,
764
+ lambda { |*args|
765
+ "#{args[0].inspect} does not have a #{args[1].inspect} attribute"
766
+ }
767
+ ],
768
+ KAXErrorActionUnsupported => [
769
+ ArgumentError,
770
+ lambda { |*args|
771
+ "#{args[0].inspect} does not have a #{args[1].inspect} action"
772
+ }
773
+ ],
774
+ KAXErrorNotificationUnsupported => [
775
+ ArgumentError,
776
+ lambda { |*args|
777
+ 'AXElements no longer supports notifications'
778
+ }
779
+ ],
780
+ KAXErrorNotImplemented => [
781
+ NotImplementedError,
782
+ lambda { |*args|
783
+ "The program that owns #{args[0].inspect} does not work with AXAPI properly"
784
+ }
785
+ ],
786
+ KAXErrorNotificationAlreadyRegistered => [
787
+ ArgumentError,
788
+ lambda { |*args|
789
+ 'AXElements no longer supports notifications'
790
+ }
791
+ ],
792
+ KAXErrorNotificationNotRegistered => [
793
+ RuntimeError,
794
+ lambda { |*args|
795
+ 'AXElements no longer supports notifications'
796
+ }
797
+ ],
798
+ KAXErrorAPIDisabled => [
799
+ RuntimeError,
800
+ lambda { |*args|
801
+ 'AXAPI has been disabled'
802
+ }
803
+ ],
804
+ KAXErrorNoValue => [
805
+ RuntimeError,
806
+ lambda { |*args|
807
+ 'AXElements internal error. ENoValue should be handled internally!'
808
+ }
809
+ ],
810
+ KAXErrorParameterizedAttributeUnsupported => [
811
+ ArgumentError,
812
+ lambda { |*args|
813
+ "#{args[0].inspect} does not have a #{args[1].inspect} parameterized attribute"
814
+ }
815
+ ],
816
+ KAXErrorNotEnoughPrecision => [
817
+ RuntimeError,
818
+ lambda { |*args|
819
+ 'AXAPI said there was not enough precision ¯\(°_o)/¯'
820
+ }
821
+ ]
822
+ }
823
+
824
+ # @param code [Number]
825
+ def handle_error code, *args
826
+ raise RuntimeError, 'assertion failed: code 0 means success!' if code.zero?
827
+ klass, handler = AXERROR.fetch code, [
828
+ RuntimeError,
829
+ lambda { |*args| "An unknown error code was returned [#{code}]:#{inspect}" }
830
+ ]
831
+ raise klass, handler.call(self, *args), caller(1)
832
+ end
833
+
834
+
835
+ # @!endgroup
836
+
837
+
838
+ ##
839
+ # @private
840
+ #
841
+ # `Pointer` type encoding for `CFArrayRef` objects
842
+ #
843
+ # @return [String]
844
+ ARRAY = '^{__CFArray}'
845
+
846
+ ##
847
+ # @private
848
+ #
849
+ # `Pointer` type encoding for `AXUIElementRef` objects
850
+ #
851
+ # @return [String]
852
+ ELEMENT = '^{__AXUIElement}'
853
+
854
+ end
855
+
856
+ # hack to find the __NSCFType class and mix things in
857
+ klass = AXUIElementCreateSystemWide().class
858
+ klass.send :include, Accessibility::Element