AXElements 0.9.0 → 1.0.0.alpha
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 +0 -4
- data/README.markdown +22 -17
- data/Rakefile +1 -1
- data/ext/accessibility/key_coder/extconf.rb +1 -1
- data/ext/accessibility/key_coder/key_coder.c +2 -4
- data/lib/accessibility.rb +3 -3
- data/lib/accessibility/core.rb +948 -0
- data/lib/accessibility/dsl.rb +30 -186
- data/lib/accessibility/enumerators.rb +1 -0
- data/lib/accessibility/factory.rb +78 -134
- data/lib/accessibility/graph.rb +5 -9
- data/lib/accessibility/highlighter.rb +86 -0
- data/lib/accessibility/{pretty_printer.rb → pp_inspector.rb} +4 -3
- data/lib/accessibility/qualifier.rb +3 -5
- data/lib/accessibility/screen_recorder.rb +217 -0
- data/lib/accessibility/statistics.rb +57 -0
- data/lib/accessibility/translator.rb +23 -32
- data/lib/accessibility/version.rb +2 -22
- data/lib/ax/application.rb +20 -159
- data/lib/ax/element.rb +42 -32
- data/lib/ax/scroll_area.rb +5 -6
- data/lib/ax/systemwide.rb +1 -33
- data/lib/ax_elements.rb +1 -9
- data/lib/ax_elements/core_graphics_workaround.rb +5 -0
- data/lib/ax_elements/nsarray_compat.rb +17 -97
- data/lib/ax_elements/vendor/inflection_data.rb +66 -0
- data/lib/ax_elements/vendor/inflections.rb +176 -0
- data/lib/ax_elements/vendor/inflector.rb +306 -0
- data/lib/minitest/ax_elements.rb +180 -0
- data/lib/mouse.rb +227 -0
- data/lib/rspec/expectations/ax_elements.rb +234 -0
- data/rakelib/gem.rake +3 -12
- data/rakelib/test.rake +15 -0
- data/test/helper.rb +20 -10
- data/test/integration/accessibility/test_core.rb +18 -0
- data/test/integration/accessibility/test_dsl.rb +40 -38
- data/test/integration/accessibility/test_enumerators.rb +1 -0
- data/test/integration/accessibility/test_graph.rb +0 -1
- data/test/integration/accessibility/test_qualifier.rb +2 -2
- data/test/integration/ax/test_application.rb +2 -9
- data/test/integration/ax/test_element.rb +0 -40
- data/test/integration/minitest/test_ax_elements.rb +89 -0
- data/test/integration/rspec/expectations/test_ax_elements.rb +102 -0
- data/test/sanity/accessibility/test_factory.rb +2 -2
- data/test/sanity/accessibility/test_highlighter.rb +56 -0
- data/test/sanity/accessibility/{test_pretty_printer.rb → test_pp_inspector.rb} +9 -9
- data/test/sanity/accessibility/test_statistics.rb +57 -0
- data/test/sanity/ax/test_application.rb +1 -16
- data/test/sanity/ax/test_element.rb +2 -2
- data/test/sanity/ax_elements/test_nsobject_inspect.rb +2 -4
- data/test/sanity/minitest/test_ax_elements.rb +17 -0
- data/test/sanity/rspec/expectations/test_ax_elements.rb +15 -0
- data/test/sanity/test_mouse.rb +22 -0
- data/test/test_core.rb +454 -0
- metadata +44 -69
- data/History.markdown +0 -41
- data/lib/accessibility/system_info.rb +0 -230
- data/lib/ax_elements/active_support_selections.rb +0 -10
- data/lib/ax_elements/mri.rb +0 -57
- data/test/sanity/accessibility/test_version.rb +0 -15
data/lib/accessibility/graph.rb
CHANGED
@@ -47,7 +47,7 @@ class Accessibility::Graph
|
|
47
47
|
|
48
48
|
def identifier
|
49
49
|
klass = @element.class.to_s.split(NAMESPACE).last
|
50
|
-
ident = @element.pp_identifier.
|
50
|
+
ident = @element.pp_identifier.dup
|
51
51
|
if ident.length > 12
|
52
52
|
ident = "#{ident[0...12]}..."
|
53
53
|
end
|
@@ -58,13 +58,9 @@ class Accessibility::Graph
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def shape
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
OVAL
|
65
|
-
else
|
66
|
-
BOX
|
67
|
-
end
|
61
|
+
(@element.attribute(:focused) && OCTAGON) ||
|
62
|
+
(@element.actions.empty? && OVAL) ||
|
63
|
+
BOX
|
68
64
|
end
|
69
65
|
|
70
66
|
def style
|
@@ -73,7 +69,7 @@ class Accessibility::Graph
|
|
73
69
|
return FILLED unless @element.attribute(:enabled)
|
74
70
|
end
|
75
71
|
# bold if focused and no children
|
76
|
-
if @element.
|
72
|
+
if @element.attribute(:focused)
|
77
73
|
return BOLD if @element.size_of(:children).zero?
|
78
74
|
end
|
79
75
|
SOLID
|
@@ -0,0 +1,86 @@
|
|
1
|
+
framework 'AppKit'
|
2
|
+
require 'accessibility/version'
|
3
|
+
|
4
|
+
##
|
5
|
+
# A screen highlighter for debugging. When you initialize a highligter
|
6
|
+
# object it will highlight the given bounds on the screen.
|
7
|
+
#
|
8
|
+
# Highligter objects can have their colour configured at initialization,
|
9
|
+
# and can also have a timeout to automatically stop displaying.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
#
|
13
|
+
# h = Accessibility::Highlighter.new(CGRectMake(100,100,100,100))
|
14
|
+
# # when you are done...
|
15
|
+
# h.stop
|
16
|
+
#
|
17
|
+
class Accessibility::Highlighter < NSWindow
|
18
|
+
|
19
|
+
# @param bounds [CGRect]
|
20
|
+
# @param opts [Hash]
|
21
|
+
# @option opts [Number] :timeout
|
22
|
+
# @option opts [NSColor] :colour (NSColor.magentaColor)
|
23
|
+
def initialize bounds, opts = {}
|
24
|
+
colour = opts[:colour] || opts[:color] || NSColor.magentaColor
|
25
|
+
|
26
|
+
bounds.flip! # we assume the rect is in the other co-ordinate system
|
27
|
+
|
28
|
+
initWithContentRect bounds,
|
29
|
+
styleMask: NSBorderlessWindowMask,
|
30
|
+
backing: NSBackingStoreBuffered,
|
31
|
+
defer: true
|
32
|
+
setOpaque false
|
33
|
+
setAlphaValue 0.20
|
34
|
+
setLevel NSStatusWindowLevel
|
35
|
+
setBackgroundColor colour
|
36
|
+
setIgnoresMouseEvents true
|
37
|
+
setFrame bounds, display: false
|
38
|
+
makeKeyAndOrderFront NSApp
|
39
|
+
|
40
|
+
if opts.has_key? :timeout
|
41
|
+
Dispatch::Queue.new(queue_id).after opts[:timeout] do
|
42
|
+
self.stop
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Tell the highlighter to stop displaying.
|
49
|
+
#
|
50
|
+
# @return [self]
|
51
|
+
def stop
|
52
|
+
close
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def queue_id
|
59
|
+
"com.marketcircle.axelements.window_killer_#{hash}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
##
|
65
|
+
# AXElements extensions to `CGRect`.
|
66
|
+
class CGRect
|
67
|
+
##
|
68
|
+
# Treats the rect as belonging to one co-ordinate system and then
|
69
|
+
# converts it to the other system.
|
70
|
+
#
|
71
|
+
# This is useful because accessibility API's expect to work with
|
72
|
+
# the flipped co-ordinate system (origin in top left), but AppKit
|
73
|
+
# prefers to use the cartesian co-ordinate system (origin in bottom
|
74
|
+
# left).
|
75
|
+
#
|
76
|
+
# @return [CGRect]
|
77
|
+
def flip!
|
78
|
+
screen_height = NSMaxY(NSScreen.mainScreen.frame)
|
79
|
+
origin.y = screen_height - NSMaxY(self)
|
80
|
+
self
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
# Initialize the shared application so that windows can be created
|
86
|
+
NSApplication.sharedApplication
|
@@ -10,13 +10,13 @@
|
|
10
10
|
# - `#attribute` returns the value of a given attribute
|
11
11
|
# - `#size_of` returns the size for an attribute
|
12
12
|
#
|
13
|
-
module Accessibility::
|
13
|
+
module Accessibility::PPInspector
|
14
14
|
|
15
15
|
##
|
16
16
|
# Create an identifier for the receiver by using various attributes
|
17
17
|
# that should make it very easy to identify the element.
|
18
18
|
#
|
19
|
-
# @return [String
|
19
|
+
# @return [String]
|
20
20
|
def pp_identifier
|
21
21
|
# @note use, or lack of use, of #inspect is intentional for visual effect
|
22
22
|
|
@@ -49,7 +49,8 @@ module Accessibility::PrettyPrinter
|
|
49
49
|
return " id=#{attribute(:identifier)}"
|
50
50
|
end
|
51
51
|
|
52
|
-
|
52
|
+
# @todo should we have other fallbacks?
|
53
|
+
return EMPTY_STRING
|
53
54
|
end
|
54
55
|
|
55
56
|
##
|
@@ -1,6 +1,4 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
|
-
|
3
|
-
require 'active_support/core_ext/object/blank'
|
4
2
|
require 'accessibility/translator'
|
5
3
|
|
6
4
|
##
|
@@ -31,7 +29,7 @@ class Accessibility::Qualifier
|
|
31
29
|
@klass = TRANSLATOR.classify(klass)
|
32
30
|
@criteria = criteria
|
33
31
|
@block = Proc.new if block_given?
|
34
|
-
compile
|
32
|
+
compile criteria
|
35
33
|
end
|
36
34
|
|
37
35
|
##
|
@@ -65,8 +63,8 @@ class Accessibility::Qualifier
|
|
65
63
|
# {#qualifies?}.
|
66
64
|
#
|
67
65
|
# @param criteria [Hash]
|
68
|
-
def compile
|
69
|
-
@filters =
|
66
|
+
def compile criteria
|
67
|
+
@filters = criteria.map do |key, value|
|
70
68
|
if value.kind_of? Hash
|
71
69
|
[:subsearch, key, value]
|
72
70
|
elsif key.kind_of? Array
|
@@ -0,0 +1,217 @@
|
|
1
|
+
framework 'AVFoundation'
|
2
|
+
require 'accessibility/version'
|
3
|
+
require 'ax_elements/core_graphics_workaround'
|
4
|
+
|
5
|
+
##
|
6
|
+
# Screen recordings, easy as pie.
|
7
|
+
#
|
8
|
+
# Things that you need to be concerned about:
|
9
|
+
# - screen going to sleep
|
10
|
+
# - short recordings (~1 second) don't work too well; it looks like
|
11
|
+
# the last bit of the buffer does not get saved so the last ~0.5
|
12
|
+
# seconds are not saved to disk (we could add a 0.5 second sleep)
|
13
|
+
# - small memory leak when a recording starts on Mountain Lion (GC)
|
14
|
+
# - constantly leaking memory during recording on Lion (GC)
|
15
|
+
# - run loop hack is not needed if code is already being called from
|
16
|
+
# in a run loop
|
17
|
+
# - pausing is not working...not sure why
|
18
|
+
#
|
19
|
+
class Accessibility::ScreenRecorder
|
20
|
+
|
21
|
+
##
|
22
|
+
# Record the screen while executing the given block. The path to the
|
23
|
+
# recording will be returned.
|
24
|
+
#
|
25
|
+
# The recorder object is yielded.
|
26
|
+
#
|
27
|
+
# @yield
|
28
|
+
# @yieldparam recorder [ScreenRecorder]
|
29
|
+
# @return [String]
|
30
|
+
def self.record file_name = nil
|
31
|
+
raise 'block required' unless block_given?
|
32
|
+
|
33
|
+
recorder = new
|
34
|
+
file_name ? recorder.start(file_name) : recorder.start
|
35
|
+
yield recorder
|
36
|
+
recorder.file
|
37
|
+
|
38
|
+
ensure
|
39
|
+
recorder.stop
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Path to the screen recording. This is `nil` until the screen
|
44
|
+
# recording begins.
|
45
|
+
#
|
46
|
+
# @return [String]
|
47
|
+
attr_reader :file
|
48
|
+
|
49
|
+
##
|
50
|
+
# @todo Expose configuration options at initialie time
|
51
|
+
def initialize
|
52
|
+
@session = AVCaptureSession.alloc.init
|
53
|
+
|
54
|
+
@input = AVCaptureScreenInput.alloc.initWithDisplayID CGMainDisplayID()
|
55
|
+
@input.capturesMouseClicks = true
|
56
|
+
|
57
|
+
@output = AVCaptureMovieFileOutput.alloc.init
|
58
|
+
@output.setDelegate self
|
59
|
+
|
60
|
+
@session.addInput @input
|
61
|
+
@session.addOutput @output
|
62
|
+
|
63
|
+
@sema = Dispatch::Semaphore.new 0
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# Synchrnously start recording. You can optionally specify a file
|
68
|
+
# name for the recording; if you do not then a default name will be
|
69
|
+
# provided in the form `~/Movies/TestRecording-20121017123230.mov`
|
70
|
+
# (the timestamp will be different for you).
|
71
|
+
#
|
72
|
+
# @param file_name [String]
|
73
|
+
def start file_name = default_file_name
|
74
|
+
@file = default_file_name
|
75
|
+
file_url = NSURL.fileURLWithPath @file, isDirectory: false
|
76
|
+
|
77
|
+
@session.startRunning
|
78
|
+
@output.startRecordingToOutputFileURL file_url,
|
79
|
+
recordingDelegate: self
|
80
|
+
|
81
|
+
@sema.wait
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Whether or not the recording has begun. This will be `true`
|
86
|
+
# after calling {#start} until {#stop} is called. It will be
|
87
|
+
# `true` while the recording is paused.
|
88
|
+
def started?
|
89
|
+
@output.recording?
|
90
|
+
end
|
91
|
+
|
92
|
+
# ##
|
93
|
+
# # Whether or not the recording has been paused.
|
94
|
+
# def paused?
|
95
|
+
# @output.paused?
|
96
|
+
# end
|
97
|
+
|
98
|
+
##
|
99
|
+
# Duration of the recording, in seconds.
|
100
|
+
#
|
101
|
+
# @return [Float]
|
102
|
+
def length
|
103
|
+
duration = @output.recordedDuration
|
104
|
+
(duration.value.to_f / duration.timescale.to_f)
|
105
|
+
end
|
106
|
+
|
107
|
+
##
|
108
|
+
# Size of the recording on disk, in bytes.
|
109
|
+
#
|
110
|
+
# @return [Fixnum]
|
111
|
+
def size
|
112
|
+
@output.recordedFileSize
|
113
|
+
end
|
114
|
+
|
115
|
+
# ##
|
116
|
+
# # Synchronously pause the recording. You can optionally pass a block
|
117
|
+
# # to this method.
|
118
|
+
# #
|
119
|
+
# # If you pass a block, the recording is paused so that the block
|
120
|
+
# # can execute and recording resumes after the block finishes. If
|
121
|
+
# # you do not pass a block then the recording is paused until you
|
122
|
+
# # call {#resume} on the receiver.
|
123
|
+
# #
|
124
|
+
# # @yield Optionally pass a block
|
125
|
+
# def pause
|
126
|
+
# @output.pauseRecording
|
127
|
+
# wait_for_callback
|
128
|
+
# @sema.wait
|
129
|
+
|
130
|
+
# if block_given?
|
131
|
+
# yield
|
132
|
+
# resume
|
133
|
+
# end
|
134
|
+
# end
|
135
|
+
|
136
|
+
# ##
|
137
|
+
# # Synchronously resume a {#pause}d recording.
|
138
|
+
# def resume
|
139
|
+
# @output.resumeRecording
|
140
|
+
# wait_for_callback
|
141
|
+
# @sema.wait
|
142
|
+
# end
|
143
|
+
|
144
|
+
##
|
145
|
+
# Synchronously stop recording and finish up commiting any data to disk.
|
146
|
+
# A recording cannot be {#start}ed again after it has been stopped; if
|
147
|
+
# you want to pause a recording then you should use {#pause} instead.
|
148
|
+
def stop
|
149
|
+
@session.stopRunning
|
150
|
+
@output.stopRecording
|
151
|
+
@sema.wait
|
152
|
+
wait_for_callback
|
153
|
+
@sema.wait
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
# @!group AVCaptureFileOutputDelegate
|
158
|
+
|
159
|
+
def captureOutput captureOutput, didOutputSampleBuffer:sampleBuffer, fromConnection:connection
|
160
|
+
# gets called for every chunk of the recording getting committed to disk
|
161
|
+
end
|
162
|
+
|
163
|
+
def captureOutput captureOutput, didDropSampleBuffer:sampleBuffer, fromConnection:connection
|
164
|
+
NSLog("Error: dropped same data from recording")
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
# @!group AVCaptureFileOutputRecordingDelegate
|
169
|
+
|
170
|
+
def captureOutput captureOutput, didFinishRecordingToOutputFileAtURL:outputFileURL, fromConnections:connections, error:error
|
171
|
+
NSLog('Finishing')
|
172
|
+
CFRunLoopStop(CFRunLoopGetCurrent())
|
173
|
+
@sema.signal
|
174
|
+
end
|
175
|
+
|
176
|
+
def captureOutput captureOutput, didPauseRecordingToOutputFileAtURL:fileURL, fromConnections:connections
|
177
|
+
NSLog('Pausing')
|
178
|
+
CFRunLoopStop(CFRunLoopGetCurrent())
|
179
|
+
@sema.signal
|
180
|
+
end
|
181
|
+
|
182
|
+
def captureOutput captureOutput, didResumeRecordingToOutputFileAtURL:fileURL, fromConnections:connections
|
183
|
+
NSLog('Resuming')
|
184
|
+
CFRunLoopStop(CFRunLoopGetCurrent())
|
185
|
+
@sema.signal
|
186
|
+
end
|
187
|
+
|
188
|
+
def captureOutput captureOutput, didStartRecordingToOutputFileAtURL:fileURL, fromConnections:connections
|
189
|
+
NSLog('Starting')
|
190
|
+
@sema.signal
|
191
|
+
end
|
192
|
+
|
193
|
+
def captureOutput captureOutput, willFinishRecordingToOutputFileAtURL:fileURL, fromConnections:connections, error:error
|
194
|
+
NSLog('Will Finish')
|
195
|
+
@sema.signal
|
196
|
+
end
|
197
|
+
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
def default_file_name
|
202
|
+
date = Time.now.strftime '%Y%m%d%H%M%S'
|
203
|
+
File.expand_path("~/Movies/TestRecording-#{date}.mov")
|
204
|
+
end
|
205
|
+
|
206
|
+
def wait_for_callback
|
207
|
+
case CFRunLoopRunInMode(KCFRunLoopDefaultMode, 30, false)
|
208
|
+
when KCFRunLoopRunStopped
|
209
|
+
true
|
210
|
+
when KCFRunLoopRunTimedOut
|
211
|
+
raise 'did not get callback'
|
212
|
+
else
|
213
|
+
raise 'unexpected result from waiting for callback'
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'accessibility/version'
|
2
|
+
|
3
|
+
class Accessibility::Statistics
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@stats = Hash.new do |h,k| h[k] = 0 end
|
7
|
+
@q = Dispatch::Queue.new "com.marketcircle.axelements.stats"
|
8
|
+
end
|
9
|
+
|
10
|
+
def increment key
|
11
|
+
@q.async do
|
12
|
+
@stats[key] += 1
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
@q.sync do # must be synchronized
|
18
|
+
set_max_length
|
19
|
+
@out = output_header << output_body << "\n"
|
20
|
+
end
|
21
|
+
@out
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def set_max_length
|
28
|
+
@max_key_len = @stats.keys.map(&:length).max
|
29
|
+
@max_val_len = @stats.values.max.to_s.length
|
30
|
+
end
|
31
|
+
|
32
|
+
def dot key, val
|
33
|
+
length = 4
|
34
|
+
length += @max_key_len - key.length
|
35
|
+
length += @max_val_len - val.to_s.length
|
36
|
+
"." * length
|
37
|
+
end
|
38
|
+
|
39
|
+
def output_header
|
40
|
+
<<-EOS
|
41
|
+
######################
|
42
|
+
# AX Call Statistics #
|
43
|
+
######################
|
44
|
+
EOS
|
45
|
+
end
|
46
|
+
|
47
|
+
def output_body
|
48
|
+
pairs = @stats.to_a.sort { |x,y| y.last <=> x.last }
|
49
|
+
pairs.map do |key, val|
|
50
|
+
key.to_s << dot(key,val) << val.to_s
|
51
|
+
end.join("\n")
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Accessibility::Statistics]
|
57
|
+
STATS = Accessibility::Statistics.new
|