AXElements 0.6.0beta2 → 0.7.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +1 -2
- data/README.markdown +152 -88
- data/Rakefile +8 -103
- data/docs/Debugging.markdown +9 -2
- data/docs/KeyboardEvents.markdown +114 -49
- data/docs/Setting.markdown +1 -0
- data/docs/images/next_version.png +0 -0
- data/ext/accessibility/key_coder/extconf.rb +22 -0
- data/ext/accessibility/key_coder/key_coder.c +113 -0
- data/lib/AXElements.rb +2 -0
- data/lib/accessibility/core.rb +897 -0
- data/lib/accessibility/debug.rb +168 -0
- data/lib/accessibility/dsl.rb +697 -0
- data/lib/accessibility/enumerators.rb +104 -0
- data/lib/accessibility/errors.rb +32 -0
- data/lib/accessibility/factory.rb +153 -0
- data/lib/accessibility/graph.rb +150 -0
- data/lib/{ax_elements/inspector.rb → accessibility/pp_inspector.rb} +39 -28
- data/lib/accessibility/qualifier.rb +158 -0
- data/lib/accessibility/string.rb +494 -0
- data/lib/accessibility/translator.rb +178 -0
- data/lib/accessibility/version.rb +7 -0
- data/lib/accessibility.rb +79 -0
- data/lib/ax/application.rb +234 -0
- data/lib/{ax_elements/elements → ax}/button.rb +2 -0
- data/lib/ax/element.rb +518 -0
- data/lib/{ax_elements/elements → ax}/radio_button.rb +2 -0
- data/lib/ax/row.rb +37 -0
- data/lib/{ax_elements/elements → ax}/static_text.rb +2 -0
- data/lib/ax/systemwide.rb +86 -0
- data/lib/ax_elements/awesome_print.rb +25 -0
- data/lib/ax_elements/exception_workaround.rb +8 -0
- data/lib/ax_elements/nsarray_compat.rb +64 -0
- data/lib/ax_elements/vendor/inflection_data.rb +65 -0
- data/lib/ax_elements/vendor/inflections.rb +172 -0
- data/lib/ax_elements/vendor/inflector.rb +306 -0
- data/lib/ax_elements.rb +14 -25
- data/lib/minitest/ax_elements.rb +112 -12
- data/lib/mouse.rb +72 -46
- data/lib/rspec/expectations/ax_elements.rb +133 -6
- data/rakelib/doc.rake +13 -0
- data/rakelib/ext.rake +61 -0
- data/rakelib/gem.rake +28 -0
- data/rakelib/test.rake +53 -0
- data/test/helper.rb +11 -97
- data/test/integration/accessibility/test_core.rb +18 -0
- data/test/integration/accessibility/test_debug.rb +44 -0
- data/test/integration/accessibility/test_dsl.rb +225 -0
- data/test/integration/accessibility/test_enumerators.rb +122 -0
- data/test/integration/accessibility/test_errors.rb +38 -0
- data/test/integration/accessibility/test_notifications.rb +22 -0
- data/test/integration/accessibility/test_qualifier.rb +148 -0
- data/test/integration/ax/test_application.rb +56 -0
- data/test/integration/ax/test_element.rb +46 -0
- data/test/integration/ax/test_row.rb +23 -0
- data/test/integration/ax_elements/test_nsarray_compat.rb +43 -0
- data/test/integration/minitest/test_ax_elements.rb +98 -0
- data/test/integration/rspec/expectations/test_ax_elements.rb +58 -0
- data/test/integration/test_mouse.rb +35 -0
- data/test/sanity/accessibility/test_core.rb +553 -0
- data/test/sanity/accessibility/test_debug.rb +63 -0
- data/test/sanity/accessibility/test_dsl.rb +75 -0
- data/test/sanity/accessibility/test_errors.rb +10 -0
- data/test/sanity/accessibility/test_factory.rb +88 -0
- data/test/sanity/accessibility/test_pp_inspector.rb +110 -0
- data/test/sanity/accessibility/test_qualifier.rb +13 -0
- data/test/sanity/accessibility/test_string.rb +238 -0
- data/test/sanity/accessibility/test_translator.rb +145 -0
- data/test/sanity/ax/test_application.rb +90 -0
- data/test/sanity/ax/test_element.rb +80 -0
- data/test/sanity/ax/test_systemwide.rb +66 -0
- data/test/sanity/ax_elements/test_nsarray_compat.rb +16 -0
- data/test/sanity/ax_elements/test_nsobject_inspect.rb +11 -0
- data/test/sanity/minitest/test_ax_elements.rb +15 -0
- data/test/sanity/rspec/expectations/test_ax_elements.rb +12 -0
- data/test/sanity/test_ax_elements.rb +10 -0
- data/test/sanity/test_mouse.rb +19 -0
- metadata +111 -93
- data/LICENSE.txt +0 -25
- data/ext/key_coder/extconf.rb +0 -6
- data/ext/key_coder/key_coder.m +0 -77
- data/lib/ax_elements/accessibility/enumerators.rb +0 -104
- data/lib/ax_elements/accessibility/graph.rb +0 -118
- data/lib/ax_elements/accessibility/language.rb +0 -347
- data/lib/ax_elements/accessibility/qualifier.rb +0 -73
- data/lib/ax_elements/accessibility.rb +0 -166
- data/lib/ax_elements/core.rb +0 -541
- data/lib/ax_elements/element.rb +0 -593
- data/lib/ax_elements/elements/application.rb +0 -88
- data/lib/ax_elements/elements/row.rb +0 -30
- data/lib/ax_elements/elements/systemwide.rb +0 -46
- data/lib/ax_elements/macruby_extensions.rb +0 -255
- data/lib/ax_elements/notification.rb +0 -37
- data/lib/ax_elements/version.rb +0 -9
- data/test/elements/test_application.rb +0 -72
- data/test/elements/test_row.rb +0 -27
- data/test/elements/test_systemwide.rb +0 -38
- data/test/test_accessibility.rb +0 -127
- data/test/test_blankness.rb +0 -26
- data/test/test_core.rb +0 -448
- data/test/test_element.rb +0 -939
- data/test/test_enumerators.rb +0 -81
- data/test/test_inspector.rb +0 -130
- data/test/test_language.rb +0 -157
- data/test/test_macruby_extensions.rb +0 -303
- data/test/test_mouse.rb +0 -5
- data/test/test_search_semantics.rb +0 -143
@@ -1,166 +0,0 @@
|
|
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
|
-
dot = Graph.new(root)
|
35
|
-
dot.build!
|
36
|
-
dot.to_s
|
37
|
-
end
|
38
|
-
|
39
|
-
##
|
40
|
-
# Dump a tree to the console, indenting for each level down the
|
41
|
-
# tree that we go, and inspecting each element.
|
42
|
-
#
|
43
|
-
# @example
|
44
|
-
#
|
45
|
-
# puts Accessibility.dump(app)
|
46
|
-
#
|
47
|
-
# @return [String]
|
48
|
-
def dump element
|
49
|
-
output = element.inspect + "\n"
|
50
|
-
enum = Accessibility::DFEnumerator.new(element)
|
51
|
-
enum.each_with_height do |element, depth|
|
52
|
-
output << "\t"*depth + element.inspect + "\n"
|
53
|
-
end
|
54
|
-
output
|
55
|
-
end
|
56
|
-
|
57
|
-
# @group Finding an object at a point
|
58
|
-
|
59
|
-
##
|
60
|
-
# Get the current mouse position and return the top most element at
|
61
|
-
# that point.
|
62
|
-
#
|
63
|
-
# @return [AX::Element]
|
64
|
-
def element_under_mouse
|
65
|
-
element_at_point Mouse.current_position
|
66
|
-
end
|
67
|
-
|
68
|
-
##
|
69
|
-
# Get the top most object at an arbitrary point on the screen.
|
70
|
-
#
|
71
|
-
# @overload element_at_point(x,y)
|
72
|
-
# @param [Float] x
|
73
|
-
# @param [Float] y
|
74
|
-
#
|
75
|
-
# @overload element_at_point([x,y])
|
76
|
-
# @param [Array(Float,Float)] point
|
77
|
-
#
|
78
|
-
# @overload element_at_point(CGPoint.new(x,y))
|
79
|
-
# @param [CGPoint] point
|
80
|
-
#
|
81
|
-
# @return [AX::Element]
|
82
|
-
def element_at_point *point
|
83
|
-
arg = point.size == 1 ? point.first : point
|
84
|
-
AX::Element.process AX.element_at_point(*arg.to_a.flatten)
|
85
|
-
end
|
86
|
-
alias_method :element_at_position, :element_at_point
|
87
|
-
|
88
|
-
# @group Finding an application object
|
89
|
-
|
90
|
-
##
|
91
|
-
# @todo Find a way for this method to work without sleeping;
|
92
|
-
# consider looping begin/rescue/end until AX starts up
|
93
|
-
# @todo This needs to handle bad bundle identifier's gracefully
|
94
|
-
#
|
95
|
-
# This is the standard way of creating an application object. It will
|
96
|
-
# launch the app if it is not already running and then create the
|
97
|
-
# accessibility object.
|
98
|
-
#
|
99
|
-
# However, this method is a HUGE hack in cases where the app is not
|
100
|
-
# already running; I've tried to register for notifications, launch
|
101
|
-
# synchronously, etc., but there is always a problem with accessibility
|
102
|
-
# not being ready. Hopefully this problem will go away on Lion...
|
103
|
-
#
|
104
|
-
# If this method fails to find an app with the appropriate bundle
|
105
|
-
# identifier then it will return nil, eventually.
|
106
|
-
#
|
107
|
-
# @param [String] bundle a bundle identifier
|
108
|
-
# @param [Float] sleep_time how long to wait between polling
|
109
|
-
# @return [AX::Application,nil]
|
110
|
-
def application_with_bundle_identifier bundle, sleep_time = 2
|
111
|
-
sleep_count = 0
|
112
|
-
while (apps = NSRunningApplication.runningApplicationsWithBundleIdentifier bundle).empty?
|
113
|
-
launch_application bundle
|
114
|
-
return if sleep_count > 10
|
115
|
-
sleep sleep_time
|
116
|
-
sleep_count += 1
|
117
|
-
end
|
118
|
-
application_with_pid apps.first.processIdentifier
|
119
|
-
end
|
120
|
-
|
121
|
-
##
|
122
|
-
# @todo We don't launch apps if they are not running, but we could if
|
123
|
-
# we used `NSWorkspace#launchApplication`, but it will be a headache
|
124
|
-
#
|
125
|
-
# Get the accessibility object for an application given its localized
|
126
|
-
# name. This will not work if the application is not already running.
|
127
|
-
#
|
128
|
-
# @param [String] name name of the application to launch
|
129
|
-
# @return [AX::Application,nil]
|
130
|
-
def application_with_name name
|
131
|
-
NSRunLoop.currentRunLoop.runUntilDate Time.now
|
132
|
-
workspace = NSWorkspace.sharedWorkspace
|
133
|
-
app = workspace.runningApplications.find { |app| app.localizedName == name }
|
134
|
-
application_with_pid(app.processIdentifier) if app
|
135
|
-
end
|
136
|
-
|
137
|
-
##
|
138
|
-
# Get the accessibility object for an application given its PID.
|
139
|
-
#
|
140
|
-
# @return [AX::Application]
|
141
|
-
def application_with_pid pid
|
142
|
-
AX::Element.process AX.application_for_pid pid
|
143
|
-
end
|
144
|
-
|
145
|
-
# @endgroup
|
146
|
-
|
147
|
-
|
148
|
-
private
|
149
|
-
|
150
|
-
##
|
151
|
-
# This method uses asynchronous method calls to launch applications.
|
152
|
-
#
|
153
|
-
# @param [String] bundle the bundle identifier for the app
|
154
|
-
# @return [Boolean]
|
155
|
-
def launch_application bundle
|
156
|
-
NSWorkspace.sharedWorkspace.launchAppWithBundleIdentifier bundle,
|
157
|
-
options: NSWorkspaceLaunchAsync,
|
158
|
-
additionalEventParamDescriptor: nil,
|
159
|
-
launchIdentifier: nil
|
160
|
-
end
|
161
|
-
|
162
|
-
end
|
163
|
-
|
164
|
-
|
165
|
-
require 'ax_elements/accessibility/enumerators'
|
166
|
-
require 'ax_elements/accessibility/qualifier'
|
data/lib/ax_elements/core.rb
DELETED
@@ -1,541 +0,0 @@
|
|
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
|