AXElements 0.9.0 → 1.0.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
- 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
|