AXElements 0.6.0beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.yardopts +20 -0
- data/LICENSE.txt +25 -0
- data/README.markdown +150 -0
- data/Rakefile +109 -0
- data/docs/AccessibilityTips.markdown +119 -0
- data/docs/Acting.markdown +340 -0
- data/docs/Debugging.markdown +326 -0
- data/docs/Inspecting.markdown +255 -0
- data/docs/KeyboardEvents.markdown +57 -0
- data/docs/NewBehaviour.markdown +151 -0
- data/docs/Notifications.markdown +271 -0
- data/docs/Searching.markdown +250 -0
- data/docs/TestingExtensions.markdown +52 -0
- data/docs/images/AX.png +0 -0
- data/docs/images/all_the_buttons.jpg +0 -0
- data/docs/images/ui_hierarchy.dot +34 -0
- data/docs/images/ui_hierarchy.png +0 -0
- data/ext/key_coder/extconf.rb +6 -0
- data/ext/key_coder/key_coder.m +77 -0
- data/lib/ax_elements/accessibility/enumerators.rb +104 -0
- data/lib/ax_elements/accessibility/language.rb +347 -0
- data/lib/ax_elements/accessibility/qualifier.rb +73 -0
- data/lib/ax_elements/accessibility.rb +164 -0
- data/lib/ax_elements/core.rb +541 -0
- data/lib/ax_elements/element.rb +593 -0
- data/lib/ax_elements/elements/application.rb +88 -0
- data/lib/ax_elements/elements/button.rb +18 -0
- data/lib/ax_elements/elements/radio_button.rb +18 -0
- data/lib/ax_elements/elements/row.rb +30 -0
- data/lib/ax_elements/elements/static_text.rb +17 -0
- data/lib/ax_elements/elements/systemwide.rb +46 -0
- data/lib/ax_elements/inspector.rb +116 -0
- data/lib/ax_elements/macruby_extensions.rb +255 -0
- data/lib/ax_elements/notification.rb +37 -0
- data/lib/ax_elements/version.rb +9 -0
- data/lib/ax_elements.rb +30 -0
- data/lib/minitest/ax_elements.rb +19 -0
- data/lib/mouse.rb +185 -0
- data/lib/rspec/expectations/ax_elements.rb +15 -0
- data/test/elements/test_application.rb +72 -0
- data/test/elements/test_row.rb +27 -0
- data/test/elements/test_systemwide.rb +38 -0
- data/test/helper.rb +119 -0
- data/test/test_accessibility.rb +127 -0
- data/test/test_blankness.rb +26 -0
- data/test/test_core.rb +448 -0
- data/test/test_element.rb +939 -0
- data/test/test_enumerators.rb +81 -0
- data/test/test_inspector.rb +121 -0
- data/test/test_language.rb +157 -0
- data/test/test_macruby_extensions.rb +303 -0
- data/test/test_mouse.rb +5 -0
- data/test/test_search_semantics.rb +143 -0
- metadata +219 -0
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'mouse'
|
2
|
+
|
3
|
+
##
|
4
|
+
# The module that contains helpers for working with the accessibility APIs.
|
5
|
+
module Accessibility; end
|
6
|
+
|
7
|
+
class << Accessibility
|
8
|
+
|
9
|
+
# @group Debug helpers
|
10
|
+
|
11
|
+
##
|
12
|
+
# Get a list of elements, starting with an element you give, and riding
|
13
|
+
# the hierarchy up to the top level object (i.e. the {AX::Application}).
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
#
|
17
|
+
# element = AX::DOCK.list.application_dock_item
|
18
|
+
# Accessibility.path(element) # => [AX::ApplicationDockItem, AX::List, AX::Application]
|
19
|
+
#
|
20
|
+
# @param [AX::Element]
|
21
|
+
# @return [Array<AX::Element>] the path in ascending order
|
22
|
+
def path *elements
|
23
|
+
element = elements.last
|
24
|
+
return path(elements << element.parent) if element.respond_to? :parent
|
25
|
+
return elements
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Make a `dot` format graph of the tree, meant for graphing with
|
30
|
+
# GraphViz.
|
31
|
+
#
|
32
|
+
# @return [String]
|
33
|
+
def graph root
|
34
|
+
raise NotImplementedError, 'Please implement me, :('
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Dump a tree to the console, indenting for each level down the
|
39
|
+
# tree that we go, and inspecting each element.
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
#
|
43
|
+
# puts Accessibility.dump(app)
|
44
|
+
#
|
45
|
+
# @return [String]
|
46
|
+
def dump element
|
47
|
+
output = element.inspect + "\n"
|
48
|
+
enum = Accessibility::DFEnumerator.new(element)
|
49
|
+
enum.each_with_height do |element, depth|
|
50
|
+
output << "\t"*depth + element.inspect + "\n"
|
51
|
+
end
|
52
|
+
output
|
53
|
+
end
|
54
|
+
|
55
|
+
# @group Finding an object at a point
|
56
|
+
|
57
|
+
##
|
58
|
+
# Get the current mouse position and return the top most element at
|
59
|
+
# that point.
|
60
|
+
#
|
61
|
+
# @return [AX::Element]
|
62
|
+
def element_under_mouse
|
63
|
+
element_at_point Mouse.current_position
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# Get the top most object at an arbitrary point on the screen.
|
68
|
+
#
|
69
|
+
# @overload element_at_point(x,y)
|
70
|
+
# @param [Float] x
|
71
|
+
# @param [Float] y
|
72
|
+
#
|
73
|
+
# @overload element_at_point([x,y])
|
74
|
+
# @param [Array(Float,Float)] point
|
75
|
+
#
|
76
|
+
# @overload element_at_point(CGPoint.new(x,y))
|
77
|
+
# @param [CGPoint] point
|
78
|
+
#
|
79
|
+
# @return [AX::Element]
|
80
|
+
def element_at_point *point
|
81
|
+
arg = point.size == 1 ? point.first : point
|
82
|
+
AX::Element.process AX.element_at_point(*arg.to_a.flatten)
|
83
|
+
end
|
84
|
+
alias_method :element_at_position, :element_at_point
|
85
|
+
|
86
|
+
# @group Finding an application object
|
87
|
+
|
88
|
+
##
|
89
|
+
# @todo Find a way for this method to work without sleeping;
|
90
|
+
# consider looping begin/rescue/end until AX starts up
|
91
|
+
# @todo This needs to handle bad bundle identifier's gracefully
|
92
|
+
#
|
93
|
+
# This is the standard way of creating an application object. It will
|
94
|
+
# launch the app if it is not already running and then create the
|
95
|
+
# accessibility object.
|
96
|
+
#
|
97
|
+
# However, this method is a HUGE hack in cases where the app is not
|
98
|
+
# already running; I've tried to register for notifications, launch
|
99
|
+
# synchronously, etc., but there is always a problem with accessibility
|
100
|
+
# not being ready. Hopefully this problem will go away on Lion...
|
101
|
+
#
|
102
|
+
# If this method fails to find an app with the appropriate bundle
|
103
|
+
# identifier then it will return nil, eventually.
|
104
|
+
#
|
105
|
+
# @param [String] bundle a bundle identifier
|
106
|
+
# @param [Float] sleep_time how long to wait between polling
|
107
|
+
# @return [AX::Application,nil]
|
108
|
+
def application_with_bundle_identifier bundle, sleep_time = 2
|
109
|
+
sleep_count = 0
|
110
|
+
while (apps = NSRunningApplication.runningApplicationsWithBundleIdentifier bundle).empty?
|
111
|
+
launch_application bundle
|
112
|
+
return if sleep_count > 10
|
113
|
+
sleep sleep_time
|
114
|
+
sleep_count += 1
|
115
|
+
end
|
116
|
+
application_with_pid apps.first.processIdentifier
|
117
|
+
end
|
118
|
+
|
119
|
+
##
|
120
|
+
# @todo We don't launch apps if they are not running, but we could if
|
121
|
+
# we used `NSWorkspace#launchApplication`, but it will be a headache
|
122
|
+
#
|
123
|
+
# Get the accessibility object for an application given its localized
|
124
|
+
# name. This will not work if the application is not already running.
|
125
|
+
#
|
126
|
+
# @param [String] name name of the application to launch
|
127
|
+
# @return [AX::Application,nil]
|
128
|
+
def application_with_name name
|
129
|
+
NSRunLoop.currentRunLoop.runUntilDate Time.now
|
130
|
+
workspace = NSWorkspace.sharedWorkspace
|
131
|
+
app = workspace.runningApplications.find { |app| app.localizedName == name }
|
132
|
+
application_with_pid(app.processIdentifier) if app
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Get the accessibility object for an application given its PID.
|
137
|
+
#
|
138
|
+
# @return [AX::Application]
|
139
|
+
def application_with_pid pid
|
140
|
+
AX::Element.process AX.application_for_pid pid
|
141
|
+
end
|
142
|
+
|
143
|
+
# @endgroup
|
144
|
+
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
##
|
149
|
+
# This method uses asynchronous method calls to launch applications.
|
150
|
+
#
|
151
|
+
# @param [String] bundle the bundle identifier for the app
|
152
|
+
# @return [Boolean]
|
153
|
+
def launch_application bundle
|
154
|
+
NSWorkspace.sharedWorkspace.launchAppWithBundleIdentifier bundle,
|
155
|
+
options: NSWorkspaceLaunchAsync,
|
156
|
+
additionalEventParamDescriptor: nil,
|
157
|
+
launchIdentifier: nil
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
require 'ax_elements/accessibility/enumerators'
|
164
|
+
require 'ax_elements/accessibility/qualifier'
|
@@ -0,0 +1,541 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Accessibility
|
4
|
+
class << self
|
5
|
+
# @return [Logger]
|
6
|
+
attr_accessor :log
|
7
|
+
end
|
8
|
+
|
9
|
+
@log = Logger.new $stderr
|
10
|
+
@log.level = Logger::ERROR # @todo lame
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# Namespace for all the accessibility objects, as well as core
|
15
|
+
# abstraction layer that that interact with OS X Accessibility
|
16
|
+
# APIs.
|
17
|
+
module AX
|
18
|
+
@ignore_notifs = true
|
19
|
+
@notifs = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# @todo The current strategy dealing with errors is just to log them,
|
24
|
+
# but that may not always be the correct thing to do. The core
|
25
|
+
# has to be refactored around this issue to become more robust.
|
26
|
+
# I've already started doing this for newer APIs but the old ones
|
27
|
+
# are important as well.
|
28
|
+
# @todo I feel a bit weird having to instantiate a new pointer every
|
29
|
+
# time I want to fetch an attribute. Since allocations are costly
|
30
|
+
# it hurts performance a lot when it comes to searches. I wonder if
|
31
|
+
# it would pay off to have a pool of pointers...
|
32
|
+
#
|
33
|
+
# The singleton methods for the AX module represent the core layer of
|
34
|
+
# abstraction for AXElements.
|
35
|
+
#
|
36
|
+
# The methods provide a clean Ruby-ish interface to the low level
|
37
|
+
# CoreFoundation functions that compose the AXAPI. Doing this we can
|
38
|
+
# hide away the need to work with pointers and centralize when errors
|
39
|
+
# are logged from the low level function calls (since CoreFoundation
|
40
|
+
# uses a different pattern for that sort of thing).
|
41
|
+
#
|
42
|
+
# Ideally this API would be stateless, but I'm still working on that...
|
43
|
+
class << AX
|
44
|
+
|
45
|
+
# @group Attributes
|
46
|
+
|
47
|
+
##
|
48
|
+
# List of attributes for the given element.
|
49
|
+
#
|
50
|
+
# @param [AXUIElementRef] element low level accessibility object
|
51
|
+
# @return [Array<String>]
|
52
|
+
def attrs_of_element element
|
53
|
+
ptr = Pointer.new ARRAY
|
54
|
+
code = AXUIElementCopyAttributeNames(element, ptr)
|
55
|
+
log_error element, code unless code.zero?
|
56
|
+
ptr[0]
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Number of elements that would be returned for the given element's
|
61
|
+
# given attribute.
|
62
|
+
#
|
63
|
+
# @param [AXUIElementRef]
|
64
|
+
# @param [String] attr an attribute constant
|
65
|
+
# @return [Fixnum]
|
66
|
+
def attr_count_of_element element, attr
|
67
|
+
ptr = Pointer.new :long_long
|
68
|
+
code = AXUIElementGetAttributeValueCount(element, attr, ptr)
|
69
|
+
log_error element, attr unless code.zero?
|
70
|
+
ptr[0]
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Fetch the given attribute's value from the given element. You will
|
75
|
+
# be given raw data from this method; that is, {Boxed} objects will
|
76
|
+
# still be wrapped in a `AXValueRef`, and elements will be
|
77
|
+
# `AXUIElementRef` objects.
|
78
|
+
#
|
79
|
+
# @param [AXUIElementRef]
|
80
|
+
# @param [String] attr an attribute constant
|
81
|
+
def attr_of_element element, attr
|
82
|
+
ptr = Pointer.new :id
|
83
|
+
code = AXUIElementCopyAttributeValue(element, attr, ptr)
|
84
|
+
log_error element, code unless code.zero?
|
85
|
+
ptr[0]
|
86
|
+
end
|
87
|
+
|
88
|
+
##
|
89
|
+
# @todo Should we handle cases where a subrole has a value of
|
90
|
+
# 'Unknown'? What is the performance impact?
|
91
|
+
#
|
92
|
+
# Fetch subrole and role of an object, pass back an array with the
|
93
|
+
# subrole first if it exists.
|
94
|
+
#
|
95
|
+
# @param [AXUIElementRef]
|
96
|
+
# @param [Array<String>]
|
97
|
+
# @return [Array<String>]
|
98
|
+
def role_for element, attrs
|
99
|
+
ptr = Pointer.new :id
|
100
|
+
AXUIElementCopyAttributeValue(element, ROLE, ptr)
|
101
|
+
ret = [ptr[0]]
|
102
|
+
if attrs.include? SUBROLE
|
103
|
+
AXUIElementCopyAttributeValue(element, SUBROLE, ptr)
|
104
|
+
# Be careful, some things claim to have a subrole but return nil
|
105
|
+
ret.unshift ptr[0] if ptr[0]
|
106
|
+
end
|
107
|
+
#raise "Found an element that has no role: #{CFShow(element)}"
|
108
|
+
ret
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# Ask whether or not the given attribute of a given element can be
|
113
|
+
# changed using the accessibility APIs.
|
114
|
+
#
|
115
|
+
# @param [AXUIElementRef]
|
116
|
+
# @param [String] attr an attribute constant
|
117
|
+
def attr_of_element_writable? element, attr
|
118
|
+
ptr = Pointer.new :bool
|
119
|
+
code = AXUIElementIsAttributeSettable(element, attr, ptr)
|
120
|
+
log_error element, code unless code.zero?
|
121
|
+
ptr[0]
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# @note This method does not check writability of the attribute
|
126
|
+
# you are setting.
|
127
|
+
#
|
128
|
+
# Set the given value to the given attribute of the given element.
|
129
|
+
#
|
130
|
+
# @param [AXUIElementRef] element
|
131
|
+
# @param [String] attr an attribute constant
|
132
|
+
# @param [Object] value the new value to set on the attribute
|
133
|
+
# @return [Object] returns the value that was set
|
134
|
+
def set_attr_of_element element, attr, value
|
135
|
+
code = AXUIElementSetAttributeValue(element, attr, value)
|
136
|
+
log_error element, code unless code.zero?
|
137
|
+
value
|
138
|
+
end
|
139
|
+
|
140
|
+
# @group Actions
|
141
|
+
|
142
|
+
##
|
143
|
+
# List of actions that the given element can perform.
|
144
|
+
#
|
145
|
+
# @param [AXUIElementRef] element low level accessibility object
|
146
|
+
# @return [Array<String>]
|
147
|
+
def actions_of_element element
|
148
|
+
array_ptr = Pointer.new ARRAY
|
149
|
+
code = AXUIElementCopyActionNames(element, array_ptr)
|
150
|
+
log_error element, code unless code.zero?
|
151
|
+
array_ptr[0]
|
152
|
+
end
|
153
|
+
|
154
|
+
##
|
155
|
+
# Trigger the given action for the given element.
|
156
|
+
#
|
157
|
+
# @param [AXUIElementRef] element
|
158
|
+
# @param [String] action an action constant
|
159
|
+
# @return [Boolean] true if successful
|
160
|
+
def action_of_element element, action
|
161
|
+
code = AXUIElementPerformAction(element, action)
|
162
|
+
code.zero? ? true : (log_error(element, code); false)
|
163
|
+
end
|
164
|
+
|
165
|
+
##
|
166
|
+
# In cases where you need (or want) to simulate keyboard input, such as
|
167
|
+
# triggering hotkeys, you will need to use this method.
|
168
|
+
#
|
169
|
+
# See the documentation page on
|
170
|
+
# {file:docs/KeyboardEvents.markdown Keyboard Events}
|
171
|
+
# to get a detailed explanation on how to encode strings.
|
172
|
+
#
|
173
|
+
# @param [AXUIElementRef] element an application to post the event to, or
|
174
|
+
# the system wide accessibility object
|
175
|
+
# @param [String] string the string you want typed on the screen
|
176
|
+
def keyboard_action element, string
|
177
|
+
post_kb_events element, parse_kb_string(string)
|
178
|
+
nil
|
179
|
+
end
|
180
|
+
|
181
|
+
# @group Parameterized Attributes
|
182
|
+
|
183
|
+
##
|
184
|
+
# List of parameterized attributes for the given element.
|
185
|
+
#
|
186
|
+
# @param [AXUIElementRef] element low level accessibility object
|
187
|
+
# @return [Array<String>]
|
188
|
+
def param_attrs_of_element element
|
189
|
+
array_ptr = Pointer.new ARRAY
|
190
|
+
code = AXUIElementCopyParameterizedAttributeNames(element, array_ptr)
|
191
|
+
log_error element, code unless code.zero?
|
192
|
+
array_ptr[0]
|
193
|
+
end
|
194
|
+
|
195
|
+
##
|
196
|
+
# Fetch the given attribute's value from the given element using the given
|
197
|
+
# parameter. You will be given raw data from this method; that is, {Boxed}
|
198
|
+
# objects will still be wrapped in a `AXValueRef`, etc.
|
199
|
+
#
|
200
|
+
# @param [AXUIElementRef] element
|
201
|
+
# @param [String] attr an attribute constant
|
202
|
+
def param_attr_of_element element, attr, param
|
203
|
+
ptr = Pointer.new :id
|
204
|
+
code = AXUIElementCopyParameterizedAttributeValue(element, attr, param, ptr)
|
205
|
+
log_error element, code unless code.zero?
|
206
|
+
ptr[0]
|
207
|
+
end
|
208
|
+
|
209
|
+
# @group Notifications
|
210
|
+
|
211
|
+
##
|
212
|
+
# @todo This method is too big, needs refactoring. It's own class?
|
213
|
+
#
|
214
|
+
# {file:docs/Notifications.markdown Notifications} are a way to put
|
215
|
+
# non-polling delays into your scripts.
|
216
|
+
#
|
217
|
+
# Use this method to register to be notified of the specified event in
|
218
|
+
# an application. You must also pass a block to this method to validate
|
219
|
+
# the notification.
|
220
|
+
#
|
221
|
+
# @param [AXUIElementRef] ref the element which will send the notification
|
222
|
+
# @param [String] name the name of the notification
|
223
|
+
# @yield Validate the notification; the block should return truthy if
|
224
|
+
# the notification received is the expected one and the script can stop
|
225
|
+
# waiting, otherwise should return falsy.
|
226
|
+
# @yieldparam [AXUIElementRef] element the element that sent the notification
|
227
|
+
# @yieldparam [String] notif the name of the notification
|
228
|
+
# @yieldreturn [Boolean] determines if the script should continue or wait
|
229
|
+
# @return [Array(Observer, AXUIElementRef, String)] the registration triple
|
230
|
+
def register_for_notif ref, name, &block
|
231
|
+
run_loop = CFRunLoopGetCurrent()
|
232
|
+
|
233
|
+
# we are ignoring the context pointer since this is OO
|
234
|
+
callback = Proc.new do |observer, element, notif, _|
|
235
|
+
LOCK.synchronize do
|
236
|
+
Accessibility.log.debug "Received notif (#{notif}) for (#{element})"
|
237
|
+
break if @ignore_notifs
|
238
|
+
break unless block.call(element, notif)
|
239
|
+
|
240
|
+
@ignore_notifs = true
|
241
|
+
source = AXObserverGetRunLoopSource(observer)
|
242
|
+
CFRunLoopRemoveSource(run_loop, source, KCFRunLoopDefaultMode)
|
243
|
+
unregister_notif_callback observer, element, notif
|
244
|
+
CFRunLoopStop(run_loop)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
dude = make_observer_for ref, callback
|
249
|
+
source = AXObserverGetRunLoopSource(dude)
|
250
|
+
register_notif_callback dude, ref, name
|
251
|
+
CFRunLoopAddSource(run_loop, source, KCFRunLoopDefaultMode)
|
252
|
+
@ignore_notifs = false
|
253
|
+
|
254
|
+
# must keep [element, observer, notif] in order to do unregistration
|
255
|
+
@notifs[dude] = [ref, name]
|
256
|
+
[dude, ref, name]
|
257
|
+
end
|
258
|
+
|
259
|
+
##
|
260
|
+
# @todo Is it safe to end the run loop when _any_ source is handled or
|
261
|
+
# should we continue to kill the run loop when the callback is
|
262
|
+
# received?
|
263
|
+
#
|
264
|
+
# Pause execution of the program until a notification is received or a
|
265
|
+
# timeout occurs.
|
266
|
+
#
|
267
|
+
# @param [Float] timeout
|
268
|
+
# @return [Boolean] true if the notification was received, otherwise false
|
269
|
+
def wait_for_notif timeout
|
270
|
+
# We use RunInMode because it has timeout functionality, return values are
|
271
|
+
case CFRunLoopRunInMode(KCFRunLoopDefaultMode, timeout, false)
|
272
|
+
when KCFRunLoopRunStopped # Stopped with CFRunLoopStop.
|
273
|
+
true
|
274
|
+
when KCFRunLoopRunTimedOut # Time interval seconds passed.
|
275
|
+
false
|
276
|
+
when KCFRunLoopFinished # Mode has no sources or timers.
|
277
|
+
raise 'Something went wrong with setting up the run loop'
|
278
|
+
when KCFRunLoopRunHandledSource
|
279
|
+
# Only applies when returnAfterSourceHandled is true.
|
280
|
+
raise 'This should never happen'
|
281
|
+
else
|
282
|
+
raise 'You just found a an OS X bug (or a MacRuby bug)...'
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
##
|
287
|
+
# @todo Flush any waiting notifs?
|
288
|
+
#
|
289
|
+
# Cancel _all_ notification registrations. Simple and clean, but a
|
290
|
+
# blunt tool at best. I didn't have time to figure out a better
|
291
|
+
# system :(
|
292
|
+
#
|
293
|
+
# @return [nil]
|
294
|
+
def unregister_notifs
|
295
|
+
LOCK.synchronize do
|
296
|
+
@ignore_notifs = true
|
297
|
+
@notifs.each_pair do |observer, pair|
|
298
|
+
unregister_notif_callback observer, *pair
|
299
|
+
end
|
300
|
+
@notifs = {}
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
# @group Element Entry Points
|
305
|
+
|
306
|
+
##
|
307
|
+
# This will give you the UI element located at the position given. If
|
308
|
+
# more than one element is at the position then the z-order of the
|
309
|
+
# elements will be used to determine which is "on top".
|
310
|
+
#
|
311
|
+
# The coordinates should be specified using the flipped coordinate
|
312
|
+
# system (origin is in the top-left, increasing downward as if reading
|
313
|
+
# a book in English).
|
314
|
+
#
|
315
|
+
# @param [Float]
|
316
|
+
# @param [Float]
|
317
|
+
# @return [AXUIElementRef]
|
318
|
+
def element_at_point x, y
|
319
|
+
ptr = Pointer.new ELEMENT
|
320
|
+
system = AXUIElementCreateSystemWide()
|
321
|
+
code = AXUIElementCopyElementAtPosition(system, x, y, ptr)
|
322
|
+
log_error system, code unless code.zero?
|
323
|
+
ptr[0]
|
324
|
+
end
|
325
|
+
|
326
|
+
##
|
327
|
+
# You can call this method to create the application object given
|
328
|
+
# the process identifier of the app.
|
329
|
+
#
|
330
|
+
# @param [Fixnum] pid process identifier for the application you want
|
331
|
+
# @return [AXUIElementRef]
|
332
|
+
def application_for_pid pid
|
333
|
+
raise ArgumentError, 'pid must be greater than 0' unless pid > 0
|
334
|
+
AXUIElementCreateApplication(pid)
|
335
|
+
end
|
336
|
+
|
337
|
+
# @group Misc.
|
338
|
+
|
339
|
+
##
|
340
|
+
# Get the PID of the application that the given element belongs to.
|
341
|
+
#
|
342
|
+
# @param [AXUIElementRef] element
|
343
|
+
# @return [Fixnum]
|
344
|
+
def pid_of_element element
|
345
|
+
ptr = Pointer.new :int
|
346
|
+
code = AXUIElementGetPid(element, ptr)
|
347
|
+
log_error element, code unless code.zero?
|
348
|
+
ptr[0]
|
349
|
+
end
|
350
|
+
|
351
|
+
# @endgroup
|
352
|
+
|
353
|
+
|
354
|
+
private
|
355
|
+
|
356
|
+
##
|
357
|
+
# @private
|
358
|
+
#
|
359
|
+
# Pointer type encoding for `CFArrayRef` objects.
|
360
|
+
#
|
361
|
+
# @return [String]
|
362
|
+
ARRAY = '^{__CFArray}'.freeze
|
363
|
+
|
364
|
+
##
|
365
|
+
# @private
|
366
|
+
#
|
367
|
+
# Pointer type encoding for `AXUIElementRef` objects.
|
368
|
+
#
|
369
|
+
# @return [String]
|
370
|
+
ELEMENT = '^{__AXUIElement}'.freeze
|
371
|
+
|
372
|
+
##
|
373
|
+
# @private
|
374
|
+
#
|
375
|
+
# Pointer type encoding for `AXObserverRef` objects.
|
376
|
+
#
|
377
|
+
# @return [String]
|
378
|
+
OBSERVER = '^{__AXObserver}'.freeze
|
379
|
+
|
380
|
+
##
|
381
|
+
# @private
|
382
|
+
#
|
383
|
+
# Local copy of a Cocoa constant; this is a performance hack.
|
384
|
+
#
|
385
|
+
# @return [String]
|
386
|
+
ROLE = KAXRoleAttribute
|
387
|
+
|
388
|
+
##
|
389
|
+
# @private
|
390
|
+
#
|
391
|
+
# Local copy of a Cocoa constant; this is a performance hack.
|
392
|
+
#
|
393
|
+
# @return [String]
|
394
|
+
SUBROLE = KAXSubroleAttribute
|
395
|
+
|
396
|
+
# @group Notifications
|
397
|
+
|
398
|
+
##
|
399
|
+
# @todo Would a Dispatch::Semaphore be better?
|
400
|
+
#
|
401
|
+
# Semaphore used to synchronize async notification stuff.
|
402
|
+
#
|
403
|
+
# @return [Mutex]
|
404
|
+
LOCK = Mutex.new
|
405
|
+
|
406
|
+
# @endgroup
|
407
|
+
|
408
|
+
##
|
409
|
+
# Map of characters to keycodes. The map is generated at boot time in
|
410
|
+
# order to support multiple keyboard layouts.
|
411
|
+
#
|
412
|
+
# @return [Hash]
|
413
|
+
KEYCODE_MAP = {}
|
414
|
+
require 'key_coder'
|
415
|
+
|
416
|
+
##
|
417
|
+
# Parse a string into a list of keyboard events to be executed in
|
418
|
+
# the given order.
|
419
|
+
#
|
420
|
+
# @param [String]
|
421
|
+
# @return [Array<Array(Number,Boolean)>]
|
422
|
+
def parse_kb_string string
|
423
|
+
sequence = []
|
424
|
+
string.each_char do |char|
|
425
|
+
if char.match(/[A-Z]/)
|
426
|
+
code = AX::KEYCODE_MAP[char.downcase]
|
427
|
+
event = [[56,true], [code,true], [code,false], [56,false]]
|
428
|
+
else
|
429
|
+
code = AX::KEYCODE_MAP[char]
|
430
|
+
event = [[code,true],[code,false]]
|
431
|
+
end
|
432
|
+
sequence.concat event
|
433
|
+
end
|
434
|
+
sequence
|
435
|
+
end
|
436
|
+
|
437
|
+
##
|
438
|
+
# Post the list of given keyboard events to the given element.
|
439
|
+
#
|
440
|
+
# @param [AXUIElementRef] element must be an application or the
|
441
|
+
# system-wide object
|
442
|
+
# @param [Array<Array(Number,Boolean)>]
|
443
|
+
def post_kb_events element, events
|
444
|
+
events.each do |event|
|
445
|
+
code = AXUIElementPostKeyboardEvent(element, 0, *event)
|
446
|
+
log_error element, code unless code.zero?
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
##
|
451
|
+
# @private
|
452
|
+
#
|
453
|
+
# A mapping of the `AXError` constants to human readable strings, though
|
454
|
+
# this has to be actively maintained in case of changes to Apple's
|
455
|
+
# documentation in the future.
|
456
|
+
#
|
457
|
+
# @return [Hash{Fixnum=>String}]
|
458
|
+
AXError = {
|
459
|
+
KAXErrorFailure => 'Generic Failure',
|
460
|
+
KAXErrorIllegalArgument => 'Illegal Argument',
|
461
|
+
KAXErrorInvalidUIElement => 'Invalid UI Element',
|
462
|
+
KAXErrorInvalidUIElementObserver => 'Invalid UI Element Observer',
|
463
|
+
KAXErrorCannotComplete => 'Cannot Complete',
|
464
|
+
KAXErrorAttributeUnsupported => 'Attribute Unsupported',
|
465
|
+
KAXErrorActionUnsupported => 'Action Unsupported',
|
466
|
+
KAXErrorNotificationUnsupported => 'Notification Unsupported',
|
467
|
+
KAXErrorNotImplemented => 'Not Implemented',
|
468
|
+
KAXErrorNotificationAlreadyRegistered => 'Notification Already Registered',
|
469
|
+
KAXErrorNotificationNotRegistered => 'Notification Not Registered',
|
470
|
+
KAXErrorAPIDisabled => 'API Disabled',
|
471
|
+
KAXErrorNoValue => 'No Value',
|
472
|
+
KAXErrorParameterizedAttributeUnsupported => 'Parameterized Attribute Unsupported',
|
473
|
+
KAXErrorNotEnoughPrecision => 'Not Enough Precision'
|
474
|
+
}
|
475
|
+
|
476
|
+
##
|
477
|
+
# Uses the call stack and error code to log a message that might be
|
478
|
+
# helpful in debugging.
|
479
|
+
#
|
480
|
+
# @param [AXUIElementRef]
|
481
|
+
# @param [Fixnum] code AXError value
|
482
|
+
def log_error element, code
|
483
|
+
message = AXError[code] || 'UNKNOWN ERROR CODE'
|
484
|
+
logger = Accessibility.log
|
485
|
+
logger.warn "[#{message} (#{code})] while trying #{caller[0]}"
|
486
|
+
logger.info "Available attributes were:\n#{attrs_of_element(element)}"
|
487
|
+
logger.info "Available actions were:\n#{actions_of_element(element)}"
|
488
|
+
# @todo logger.info available parameterized attributes
|
489
|
+
logger.debug "Backtrace: #{caller.description}"
|
490
|
+
# @todo logger.debug pp hierarchy element or pp element
|
491
|
+
end
|
492
|
+
|
493
|
+
##
|
494
|
+
# Create and return a notification observer for the given object's
|
495
|
+
# application.
|
496
|
+
#
|
497
|
+
# @param [AXUIElementRef] element
|
498
|
+
# @param [Method,Proc] callback
|
499
|
+
# @return [AXObserverRef]
|
500
|
+
def make_observer_for element, callback
|
501
|
+
ptr = Pointer.new OBSERVER
|
502
|
+
code = AXObserverCreate(pid_of_element(element), callback, ptr)
|
503
|
+
log_error element, code unless code.zero?
|
504
|
+
ptr[0]
|
505
|
+
end
|
506
|
+
|
507
|
+
##
|
508
|
+
# @todo Consider exposing the refcon argument. Probably not until
|
509
|
+
# someone actually wants to pass a context around.
|
510
|
+
#
|
511
|
+
# Register an observer for a specific event.
|
512
|
+
#
|
513
|
+
# @param [AXObserverRef]
|
514
|
+
# @param [AX::Element]
|
515
|
+
# @param [String]
|
516
|
+
def register_notif_callback observer, element, notif
|
517
|
+
code = AXObserverAddNotification(observer, element, notif, nil)
|
518
|
+
log_error element, code unless code.zero?
|
519
|
+
end
|
520
|
+
|
521
|
+
##
|
522
|
+
# @todo No need to capture error code when handling all error cases
|
523
|
+
# properly. So I should get around to that soon.
|
524
|
+
#
|
525
|
+
# Unregister a notification that has been previously setup.
|
526
|
+
#
|
527
|
+
# @param [AXObserverRef]
|
528
|
+
# @param [AX::Element]
|
529
|
+
# @param [String]
|
530
|
+
def unregister_notif_callback observer, ref, notif
|
531
|
+
case code = AXObserverRemoveNotification(observer, ref, notif)
|
532
|
+
when KAXErrorNotificationNotRegistered
|
533
|
+
Accessibility.log.warn "Notif no longer registered: (#{ref}:#{notif})"
|
534
|
+
when KAXErrorIllegalArgument
|
535
|
+
raise ArgumentError, "Notif not unregistered (#{ref}:#{notif})"
|
536
|
+
else
|
537
|
+
log_error element, code unless code.zero?
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
end
|