accessibility_core 0.1.0

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