castanaut 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Copyright.txt +29 -0
- data/History.txt +3 -0
- data/Manifest.txt +31 -0
- data/README.txt +161 -0
- data/Rakefile +25 -0
- data/bin/castanaut +7 -0
- data/cbin/osxautomation +0 -0
- data/lib/castanaut/exceptions.rb +32 -0
- data/lib/castanaut/keys.rb +43 -0
- data/lib/castanaut/main.rb +20 -0
- data/lib/castanaut/movie.rb +353 -0
- data/lib/castanaut/plugin.rb +26 -0
- data/lib/castanaut.rb +64 -0
- data/lib/plugins/ishowu.rb +94 -0
- data/lib/plugins/mousepose.rb +38 -0
- data/lib/plugins/safari.rb +124 -0
- data/scripts/coords.js +48 -0
- data/scripts/gebys.js +612 -0
- data/tasks/ann.rake +77 -0
- data/tasks/annotations.rake +22 -0
- data/tasks/doc.rake +49 -0
- data/tasks/gem.rake +110 -0
- data/tasks/manifest.rake +50 -0
- data/tasks/post_load.rake +18 -0
- data/tasks/rubyforge.rake +57 -0
- data/tasks/setup.rb +221 -0
- data/tasks/spec.rake +43 -0
- data/tasks/svn.rake +44 -0
- data/tasks/test.rake +40 -0
- data/test/example_script.screenplay +91 -0
- data/test/googling.screenplay +34 -0
- metadata +87 -0
@@ -0,0 +1,353 @@
|
|
1
|
+
module Castanaut
|
2
|
+
# The movie class is the containing context within which screenplays are
|
3
|
+
# invoked. It provides a number of basic stage directions for your
|
4
|
+
# screenplays, and can be extended with plugins.
|
5
|
+
class Movie
|
6
|
+
|
7
|
+
# Runs the "screenplay", which is a file containing Castanaut instructions.
|
8
|
+
#
|
9
|
+
def initialize(screenplay)
|
10
|
+
perms_test
|
11
|
+
|
12
|
+
if !screenplay || !File.exists?(screenplay)
|
13
|
+
raise Castanaut::Exceptions::ScreenplayNotFound
|
14
|
+
end
|
15
|
+
@screenplay_path = screenplay
|
16
|
+
|
17
|
+
File.open(FILE_RUNNING, 'w') {|f| f.write('')}
|
18
|
+
|
19
|
+
begin
|
20
|
+
# We run the movie in a separate thread; in the main thread we
|
21
|
+
# continue to check the "running" file flag and kill the movie if
|
22
|
+
# it is removed.
|
23
|
+
movie = Thread.new do
|
24
|
+
begin
|
25
|
+
eval(IO.read(@screenplay_path), binding)
|
26
|
+
rescue => e
|
27
|
+
@e = e
|
28
|
+
ensure
|
29
|
+
File.unlink(FILE_RUNNING) if File.exists?(FILE_RUNNING)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
while File.exists?(FILE_RUNNING)
|
34
|
+
sleep 0.5
|
35
|
+
break unless movie.alive?
|
36
|
+
end
|
37
|
+
|
38
|
+
if movie.alive?
|
39
|
+
movie.kill
|
40
|
+
raise Castanaut::Exceptions::AbortedByUser
|
41
|
+
end
|
42
|
+
|
43
|
+
raise @e if @e
|
44
|
+
rescue => e
|
45
|
+
puts "ABNORMAL EXIT: #{e.message}\n" + e.backtrace.join("\n")
|
46
|
+
ensure
|
47
|
+
roll_credits
|
48
|
+
File.unlink(FILE_RUNNING) if File.exists?(FILE_RUNNING)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Launch the application matching the string given in the first argument.
|
53
|
+
# (This resolution is handled by Applescript.)
|
54
|
+
#
|
55
|
+
# If the options hash is given, it should contain the co-ordinates for
|
56
|
+
# the window (top, left, width, height). The to method will format these
|
57
|
+
# co-ordinates appropriately.
|
58
|
+
#
|
59
|
+
def launch(app_name, *options)
|
60
|
+
options = combine_options(*options)
|
61
|
+
|
62
|
+
ensure_window = ""
|
63
|
+
case app_name.downcase
|
64
|
+
when "safari"
|
65
|
+
ensure_window = "if (count(windows)) < 1 then make new document"
|
66
|
+
end
|
67
|
+
|
68
|
+
positioning = ""
|
69
|
+
if options[:to]
|
70
|
+
pos = "#{options[:to][:left]}, #{options[:to][:top]}"
|
71
|
+
dims = "#{options[:to][:left] + options[:to][:width]}, " +
|
72
|
+
"#{options[:to][:top] + options[:to][:height]}"
|
73
|
+
if options[:to][:width]
|
74
|
+
positioning = "set bounds of front window to {#{pos}, #{dims}}"
|
75
|
+
else
|
76
|
+
positioning = "set position of front window to {#{pos}}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
execute_applescript(%Q`
|
81
|
+
tell application "#{app_name}"
|
82
|
+
activate
|
83
|
+
#{ensure_window}
|
84
|
+
#{positioning}
|
85
|
+
end tell
|
86
|
+
`)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Move the mouse cursor to the specified co-ordinates.
|
90
|
+
#
|
91
|
+
def cursor(*options)
|
92
|
+
options = combine_options(*options)
|
93
|
+
apply_offset(options)
|
94
|
+
@cursor_loc ||= {}
|
95
|
+
@cursor_loc[:x] = options[:to][:left]
|
96
|
+
@cursor_loc[:y] = options[:to][:top]
|
97
|
+
automatically "mousemove #{@cursor_loc[:x]} #{@cursor_loc[:y]}"
|
98
|
+
end
|
99
|
+
|
100
|
+
alias :move :cursor
|
101
|
+
|
102
|
+
# Send a mouse-click at the current mouse location.
|
103
|
+
#
|
104
|
+
def click(btn = 'left')
|
105
|
+
automatically "mouseclick #{mouse_button_translate(btn)}"
|
106
|
+
end
|
107
|
+
|
108
|
+
# Send a double-click at the current mouse location.
|
109
|
+
#
|
110
|
+
def doubleclick(btn = 'left')
|
111
|
+
automatically "mousedoubleclick #{mouse_button_translate(btn)}"
|
112
|
+
end
|
113
|
+
|
114
|
+
# Send a triple-click at the current mouse location.
|
115
|
+
#
|
116
|
+
def tripleclick(btn = 'left')
|
117
|
+
automatically "mousetripleclick #{mouse_button_translate(btn)}"
|
118
|
+
end
|
119
|
+
|
120
|
+
# Press the button down at the current mouse location. Does not
|
121
|
+
# release the button until the mouseup method is invoked.
|
122
|
+
#
|
123
|
+
def mousedown(btn = 'left')
|
124
|
+
automatically "mousedown #{mouse_button_translate(btn)}"
|
125
|
+
end
|
126
|
+
|
127
|
+
# Releases the mouse button pressed by a previous mousedown.
|
128
|
+
#
|
129
|
+
def mouseup(btn = 'left')
|
130
|
+
automatically "mouseup #{mouse_button_translate(btn)}"
|
131
|
+
end
|
132
|
+
|
133
|
+
# "Drags" the mouse by (effectively) issuing a mousedown at the current
|
134
|
+
# mouse location, then moving the mouse to the specified coordinates, then
|
135
|
+
# issuing a mouseup.
|
136
|
+
#
|
137
|
+
def drag(*options)
|
138
|
+
options = combine_options(*options)
|
139
|
+
apply_offset(options)
|
140
|
+
automatically "mousedrag #{options[:to][:left]} #{options[:to][:top]}"
|
141
|
+
end
|
142
|
+
|
143
|
+
# Sends the characters into the active control in the active window.
|
144
|
+
#
|
145
|
+
def type(str)
|
146
|
+
automatically "type #{str}"
|
147
|
+
end
|
148
|
+
|
149
|
+
# Sends the keycode (a hex value) to the active control in the active
|
150
|
+
# window. For more about keycode values, see Mac Developer documentation.
|
151
|
+
#
|
152
|
+
def hit(key)
|
153
|
+
automatically "hit #{key}"
|
154
|
+
end
|
155
|
+
|
156
|
+
# Don't do anything for the specified number of seconds (can be portions
|
157
|
+
# of a second).
|
158
|
+
#
|
159
|
+
def pause(seconds)
|
160
|
+
sleep seconds
|
161
|
+
end
|
162
|
+
|
163
|
+
# Use Leopard's native text-to-speech functionality to emulate a human
|
164
|
+
# voice saying the narrative text.
|
165
|
+
#
|
166
|
+
def say(narrative)
|
167
|
+
run(%Q`say "#{escape_dq(narrative)}"`)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Starts saying the narrative text, and simultaneously begins executing
|
171
|
+
# the given block. Waits until both are finished.
|
172
|
+
#
|
173
|
+
def while_saying(narrative)
|
174
|
+
if block_given?
|
175
|
+
fork { say(narrative) }
|
176
|
+
yield
|
177
|
+
Process.wait
|
178
|
+
else
|
179
|
+
say(narrative)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Get a hash representing specific screen co-ordinates. Use in combination
|
184
|
+
# with cursor, drag, launch, and similar methods.
|
185
|
+
#
|
186
|
+
def to(l, t, w = nil, h = nil)
|
187
|
+
result = {
|
188
|
+
:to => {
|
189
|
+
:left => l,
|
190
|
+
:top => t
|
191
|
+
}
|
192
|
+
}
|
193
|
+
result[:to][:width] = w if w
|
194
|
+
result[:to][:height] = h if h
|
195
|
+
result
|
196
|
+
end
|
197
|
+
|
198
|
+
alias :at :to
|
199
|
+
|
200
|
+
# Get a hash representing specific screen co-ordinates *relative to the
|
201
|
+
# current mouse location.
|
202
|
+
#
|
203
|
+
def by(x, y)
|
204
|
+
unless @cursor_loc
|
205
|
+
@cursor_loc = automatically("mouselocation").strip.split(' ')
|
206
|
+
@cursor_loc = {:x => @cursor_loc[0].to_i, :y => @cursor_loc[1].to_i}
|
207
|
+
end
|
208
|
+
to(@cursor_loc[:x] + x, @cursor_loc[:y] + y)
|
209
|
+
end
|
210
|
+
|
211
|
+
# The result of this method can be added +to+ a co-ordinates hash,
|
212
|
+
# offsetting the top and left values by the given margins.
|
213
|
+
#
|
214
|
+
def offset(x, y)
|
215
|
+
{ :offset => { :x => x, :y => y } }
|
216
|
+
end
|
217
|
+
|
218
|
+
|
219
|
+
# Returns a region hash describing the entire screen area. (May be wonky
|
220
|
+
# for multi-monitor set-ups.)
|
221
|
+
#
|
222
|
+
def screen_size
|
223
|
+
coords = execute_applescript(%Q`
|
224
|
+
tell application "Finder"
|
225
|
+
get bounds of window of desktop
|
226
|
+
end tell
|
227
|
+
`)
|
228
|
+
coords = coords.split(", ").collect {|c| c.to_i}
|
229
|
+
to(*coords)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Runs a shell command, performing fairly naive (but effective!) exit
|
233
|
+
# status handling. Returns the stdout result of the command.
|
234
|
+
#
|
235
|
+
def run(cmd)
|
236
|
+
#puts("Executing: #{cmd}")
|
237
|
+
result = `#{cmd}`
|
238
|
+
raise Castanaut::Exceptions::ExternalActionError if $?.exitstatus > 0
|
239
|
+
result
|
240
|
+
end
|
241
|
+
|
242
|
+
# Adds custom methods to this movie instance, allowing you to perform
|
243
|
+
# additional actions. See the README.txt for more information.
|
244
|
+
#
|
245
|
+
def plugin(str)
|
246
|
+
str.downcase!
|
247
|
+
begin
|
248
|
+
require File.join(File.dirname(@screenplay_path),"plugins","#{str}.rb")
|
249
|
+
rescue LoadError
|
250
|
+
require File.join(LIBPATH, "plugins", "#{str}.rb")
|
251
|
+
end
|
252
|
+
extend eval("Castanaut::Plugin::#{str.capitalize}")
|
253
|
+
end
|
254
|
+
|
255
|
+
# Loads a script from a file into a string, looking first in the
|
256
|
+
# scripts directory beneath the path where Castanaut was executed,
|
257
|
+
# and falling back to Castanaut's gem path.
|
258
|
+
#
|
259
|
+
def script(filename)
|
260
|
+
@cached_scripts ||= {}
|
261
|
+
unless @cached_scripts[filename]
|
262
|
+
fpath = File.join(File.dirname(@screenplay_path), "scripts", filename)
|
263
|
+
scpt = nil
|
264
|
+
if File.exists?(fpath)
|
265
|
+
scpt = IO.read(fpath)
|
266
|
+
else
|
267
|
+
scpt = IO.read(File.join(PATH, "scripts", filename))
|
268
|
+
end
|
269
|
+
@cached_scripts[filename] = scpt
|
270
|
+
end
|
271
|
+
|
272
|
+
@cached_scripts[filename]
|
273
|
+
end
|
274
|
+
|
275
|
+
# This stage direction is slightly different to the other ones. It collects
|
276
|
+
# a set of directions to be executed when the movie ends, or when it is
|
277
|
+
# aborted by the user. Mostly, it's used for cleaning up stuff. Here's
|
278
|
+
# an example:
|
279
|
+
#
|
280
|
+
# ishowu_start_recording
|
281
|
+
# at_end_of_movie do
|
282
|
+
# ishowu_stop_recording
|
283
|
+
# end
|
284
|
+
# move to(100, 100) # ... et cetera
|
285
|
+
#
|
286
|
+
# You can use this multiple times in your screenplay -- remember that if
|
287
|
+
# the movie is aborted by the user before this direction is used, its
|
288
|
+
# contents won't be executed. So in general, create an at_end_of_movie
|
289
|
+
# block after every action that you want to revert (like in the example
|
290
|
+
# above).
|
291
|
+
def at_end_of_movie(&blk)
|
292
|
+
@end_credits ||= []
|
293
|
+
@end_credits << blk
|
294
|
+
end
|
295
|
+
|
296
|
+
protected
|
297
|
+
def execute_applescript(scpt)
|
298
|
+
File.open(FILE_APPLESCRIPT, 'w') {|f| f.write(scpt)}
|
299
|
+
result = run("osascript #{FILE_APPLESCRIPT}")
|
300
|
+
File.unlink(FILE_APPLESCRIPT)
|
301
|
+
result
|
302
|
+
end
|
303
|
+
|
304
|
+
def automatically(cmd)
|
305
|
+
run("#{osxautomation_path} \"#{cmd}\"")
|
306
|
+
end
|
307
|
+
|
308
|
+
def escape_dq(str)
|
309
|
+
str.gsub(/\\/,'\\\\\\').gsub(/"/, '\"')
|
310
|
+
end
|
311
|
+
|
312
|
+
def combine_options(*args)
|
313
|
+
options = args.inject({}) { |result, option| result.update(option) }
|
314
|
+
end
|
315
|
+
|
316
|
+
private
|
317
|
+
def osxautomation_path
|
318
|
+
File.join(PATH, "cbin", "osxautomation")
|
319
|
+
end
|
320
|
+
|
321
|
+
def perms_test
|
322
|
+
return if File.executable?(osxautomation_path)
|
323
|
+
puts "IMPORTANT: Castanaut has recently been installed or updated. " +
|
324
|
+
"You need to give it the right to control mouse and keyboard " +
|
325
|
+
"input during screenplays."
|
326
|
+
|
327
|
+
run("sudo chmod a+x #{osxautomation_path}")
|
328
|
+
|
329
|
+
if File.executable?(osxautomation_path)
|
330
|
+
puts "Permission granted. Thanks."
|
331
|
+
else
|
332
|
+
raise Castanaut::Exceptions::OSXAutomationPermissionError
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def apply_offset(options)
|
337
|
+
return unless options[:to] && options[:offset]
|
338
|
+
options[:to][:left] += options[:offset][:x] || 0
|
339
|
+
options[:to][:top] += options[:offset][:y] || 0
|
340
|
+
end
|
341
|
+
|
342
|
+
def mouse_button_translate(btn)
|
343
|
+
return btn if btn.is_a?(Integer)
|
344
|
+
{"left" => 1, "right" => 2, "middle" => 3}[btn]
|
345
|
+
end
|
346
|
+
|
347
|
+
def roll_credits
|
348
|
+
return unless @end_credits && @end_credits.any?
|
349
|
+
@end_credits.each {|credit| credit.call}
|
350
|
+
end
|
351
|
+
|
352
|
+
end
|
353
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Castanaut
|
2
|
+
|
3
|
+
# Castanaut uses plugins to extend the available actions beyond simple
|
4
|
+
# mouse and keyboard input. Typically each plugin is application-specific.
|
5
|
+
# See the Safari, Mousepose and Ishowu plugins for examples, and review the
|
6
|
+
# README.txt for details on creating your own.
|
7
|
+
#
|
8
|
+
# In short, for a plugin called "foo", your script should have this structure:
|
9
|
+
#
|
10
|
+
# module Castanaut
|
11
|
+
# module Plugin
|
12
|
+
# module Foo
|
13
|
+
#
|
14
|
+
# # define your stage directions (ie, Movie instance methods) here.
|
15
|
+
#
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# The script must exist in a sub-directory of the screenplay's location
|
21
|
+
# called "plugins", and must be called (in this case): foo.rb.
|
22
|
+
#
|
23
|
+
module Plugin
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
data/lib/castanaut.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# $Id$
|
2
|
+
|
3
|
+
# Equivalent to a header guard in C/C++
|
4
|
+
# Used to prevent the class/module from being loaded more than once
|
5
|
+
unless defined? Castanaut
|
6
|
+
|
7
|
+
# The Castanaut module. For orienting yourself within the code, it's
|
8
|
+
# recommended you begin with the documentation for the Movie class,
|
9
|
+
# which is the big one.
|
10
|
+
#
|
11
|
+
# Execution typically begins with the Main class.
|
12
|
+
module Castanaut
|
13
|
+
|
14
|
+
# :stopdoc:
|
15
|
+
VERSION = '1.0.0'
|
16
|
+
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
17
|
+
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
|
18
|
+
|
19
|
+
FILE_RUNNING = "/tmp/castanaut.running"
|
20
|
+
FILE_APPLESCRIPT = "/tmp/castanaut.scpt"
|
21
|
+
# :startdoc:
|
22
|
+
|
23
|
+
# Returns the version string for the library.
|
24
|
+
#
|
25
|
+
def self.version
|
26
|
+
VERSION
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the library path for the module. If any arguments are given,
|
30
|
+
# they will be joined to the end of the libray path using
|
31
|
+
# <tt>File.join</tt>.
|
32
|
+
#
|
33
|
+
def self.libpath( *args )
|
34
|
+
args.empty? ? LIBPATH : ::File.join(LIBPATH, *args)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns the lpath for the module. If any arguments are given,
|
38
|
+
# they will be joined to the end of the path using
|
39
|
+
# <tt>File.join</tt>.
|
40
|
+
#
|
41
|
+
def self.path( *args )
|
42
|
+
args.empty? ? PATH : ::File.join(PATH, *args)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Utility method used to rquire all files ending in .rb that lie in the
|
46
|
+
# directory below this file that has the same name as the filename passed
|
47
|
+
# in. Optionally, a specific _directory_ name can be passed in such that
|
48
|
+
# the _filename_ does not have to be equivalent to the directory.
|
49
|
+
#
|
50
|
+
def self.require_all_libs_relative_to( fname, dir = nil )
|
51
|
+
dir ||= ::File.basename(fname, '.*')
|
52
|
+
search_me = ::File.expand_path(
|
53
|
+
::File.join(::File.dirname(fname), dir, '**', '*.rb'))
|
54
|
+
|
55
|
+
Dir.glob(search_me).sort.each {|rb| require rb}
|
56
|
+
end
|
57
|
+
|
58
|
+
end # module Castanaut
|
59
|
+
|
60
|
+
Castanaut.require_all_libs_relative_to __FILE__
|
61
|
+
|
62
|
+
end # unless defined?
|
63
|
+
|
64
|
+
# EOF
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Castanaut; module Plugin
|
2
|
+
|
3
|
+
# This module provides primitive support for iShowU, a screencast capturing
|
4
|
+
# tool for Mac OS X from Shiny White Box.
|
5
|
+
#
|
6
|
+
# iShowU is widely considered a good, simple application for its purpose,
|
7
|
+
# but you're by no means required to use it for Castanaut. Simply write
|
8
|
+
# your own module for Snapz Pro, or ScreenFlow, or whatever you like.
|
9
|
+
#
|
10
|
+
# Shiny White Box is promising much better Applescript support in an
|
11
|
+
# imminent version, which could tidy up this module quite a bit.
|
12
|
+
#
|
13
|
+
# More info: http://www.shinywhitebox.com/home/home.html
|
14
|
+
module Ishowu
|
15
|
+
|
16
|
+
# Set the screencast to capture a particular region of the screen.
|
17
|
+
# Generate appropriately-formatted co-ordinates using Castanaut::Movie#to.
|
18
|
+
def ishowu_set_region(*options)
|
19
|
+
ishowu_applescriptify
|
20
|
+
|
21
|
+
options = combine_options(*options)
|
22
|
+
|
23
|
+
ishowu_menu_item("Capture", "Capture full screen")
|
24
|
+
sleep(0.2)
|
25
|
+
ishowu_menu_item("Capture", "Capture custom area", false)
|
26
|
+
sleep(0.2)
|
27
|
+
automatically "mousewarp 4 4"
|
28
|
+
|
29
|
+
drag to(options[:to][:left], options[:to][:top])
|
30
|
+
|
31
|
+
sleep(0.2)
|
32
|
+
bounds = screen_size
|
33
|
+
automatically "mousewarp #{bounds[:to][:width]} #{bounds[:to][:height]}"
|
34
|
+
drag to(
|
35
|
+
options[:to][:left] + options[:to][:width],
|
36
|
+
options[:to][:top] + options[:to][:height]
|
37
|
+
)
|
38
|
+
hit Enter
|
39
|
+
ishowu_hide
|
40
|
+
end
|
41
|
+
|
42
|
+
# Tell iShowU to start recording. Will automatically stop recording when
|
43
|
+
# the movie is ended, unless you set :auto_stop => false in options.
|
44
|
+
def ishowu_start_recording(options = {})
|
45
|
+
ishowu_hide
|
46
|
+
ishowu_menu_item("Capture", "Start capture")
|
47
|
+
unless options[:auto_stop] == false
|
48
|
+
at_end_of_movie { ishowu_stop_recording }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Tell iShowU to stop recording.
|
53
|
+
def ishowu_stop_recording
|
54
|
+
ishowu_menu_item("Capture", "Stop capture")
|
55
|
+
end
|
56
|
+
|
57
|
+
# Execute an iShowU menu option.
|
58
|
+
def ishowu_menu_item(menu, item, quiet = true)
|
59
|
+
ascript = %Q`
|
60
|
+
tell application "iShowU"
|
61
|
+
activate
|
62
|
+
tell application "System Events"
|
63
|
+
click menu item "#{item}" of menu "#{menu}" of menu bar item "#{menu}" of menu bar 1 of process "iShowU"
|
64
|
+
#{'set visible of process "iShowU" to false' if quiet}
|
65
|
+
end tell
|
66
|
+
end
|
67
|
+
`
|
68
|
+
execute_applescript(ascript)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Hide the iShowU window. This is a bit random, and suggestions are
|
72
|
+
# welcomed.
|
73
|
+
def ishowu_hide
|
74
|
+
ishowu_menu_item("iShowU", "Hide iShowU")
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
# iShowU is not Applescript-enabled out of the box. This fix, arguably
|
79
|
+
# a hack, lets us do some limited work with it in Applescript.
|
80
|
+
def ishowu_applescriptify
|
81
|
+
execute_applescript(%Q`
|
82
|
+
try
|
83
|
+
tell application "Finder"
|
84
|
+
set the a_app to (application file id "com.tcdc.Digitizer") as alias
|
85
|
+
end tell
|
86
|
+
set the plist_filepath to the quoted form of ¬
|
87
|
+
((POSIX path of the a_app) & "Contents/Info")
|
88
|
+
do shell script "defaults write " & the plist_filepath & space ¬
|
89
|
+
& "NSAppleScriptEnabled -bool YES"
|
90
|
+
end try
|
91
|
+
`)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end; end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Castanaut; module Plugin
|
2
|
+
|
3
|
+
# This module provides actions for controlling Mousepose, a commercial
|
4
|
+
# application from Boinx Software. Basically it lets you put a halo around
|
5
|
+
# the mouse whenever a key mouse action occurs.
|
6
|
+
#
|
7
|
+
# It doesn't do any configuration of Mousepose on the fly. Configure
|
8
|
+
# Mousepose settings before running your screenplay.
|
9
|
+
#
|
10
|
+
# Tested against Mousepose 2. More info: http://www.boinx.com/mousepose
|
11
|
+
module Mousepose
|
12
|
+
|
13
|
+
# Put a halo around the mouse. If a block is given to this method,
|
14
|
+
# the halo will be turned off when the block completes. Otherwise,
|
15
|
+
# you'll have to use dim to dismiss the halo.
|
16
|
+
def highlight
|
17
|
+
execute_applescript(%Q`
|
18
|
+
tell application "Mousepose"
|
19
|
+
start effect
|
20
|
+
end
|
21
|
+
`)
|
22
|
+
if block_given?
|
23
|
+
yield
|
24
|
+
dim
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Dismiss the halo around the mouse that was invoked by a previous
|
29
|
+
# highlight method.
|
30
|
+
def dim
|
31
|
+
execute_applescript(%Q`
|
32
|
+
tell application "Mousepose"
|
33
|
+
stop effect
|
34
|
+
end
|
35
|
+
`)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end; end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module Castanaut
|
2
|
+
|
3
|
+
module Plugin
|
4
|
+
# This module provides actions for controlling Safari. It's tested against
|
5
|
+
# Safari 3 on Mac OS X 10.5.2.
|
6
|
+
module Safari
|
7
|
+
|
8
|
+
# Open a URL in the front Safari tab.
|
9
|
+
def url(str)
|
10
|
+
execute_applescript(%Q`
|
11
|
+
tell application "safari"
|
12
|
+
do JavaScript "location.href = '#{str}'" in front document
|
13
|
+
end tell
|
14
|
+
`)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Get the co-ordinates of an element in the front Safari tab. Use this
|
18
|
+
# with Castanaut::Movie#cursor to send the mouse cursor to the element.
|
19
|
+
#
|
20
|
+
# Options include:
|
21
|
+
# * :index - an integer (*n*) that gets the *n*th element matching the
|
22
|
+
# selector. Defaults to the first element.
|
23
|
+
# * :area - whereabouts in the element do you want the coordinates.
|
24
|
+
# Valid values are: left, center, right, and top, middle, bottom.
|
25
|
+
# Defaults to ["center", "middle"].
|
26
|
+
# If single axis is given (eg "left"), the other axis uses its default.
|
27
|
+
def to_element(selector, options = {})
|
28
|
+
pos = options.delete(:area)
|
29
|
+
coords = element_coordinates(selector, options)
|
30
|
+
|
31
|
+
x_axis, y_axis = [:center, :middle]
|
32
|
+
[pos].flatten.first(2).each do |p|
|
33
|
+
p = p.to_s.downcase
|
34
|
+
x_axis = p.to_sym if %w[left center right].include?(p)
|
35
|
+
y_axis = p.to_sym if %w[top middle bottom].include?(p)
|
36
|
+
end
|
37
|
+
|
38
|
+
edge_offset = options[:edge_offset] || 3
|
39
|
+
case x_axis
|
40
|
+
when :left
|
41
|
+
x = coords[0] + edge_offset
|
42
|
+
when :center
|
43
|
+
x = (coords[0] + coords[2] * 0.5).to_i
|
44
|
+
when :right
|
45
|
+
x = (coords[0] + coords[2]) - edge_offset
|
46
|
+
end
|
47
|
+
|
48
|
+
case y_axis
|
49
|
+
when :top
|
50
|
+
y = coords[1] + edge_offset
|
51
|
+
when :middle
|
52
|
+
y = (coords[1] + coords[3] * 0.5).to_i
|
53
|
+
when :bottom
|
54
|
+
y = (coords[1] + coords[3]) - edge_offset
|
55
|
+
end
|
56
|
+
|
57
|
+
result = { :to => { :left => x, :top => y } }
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
# Note: the script should set the Castanaut.result variable.
|
62
|
+
def execute_javascript(scpt)
|
63
|
+
execute_applescript %Q`
|
64
|
+
tell application "Safari"
|
65
|
+
do JavaScript "
|
66
|
+
document.oldTitle = document.title;
|
67
|
+
#{escape_dq(scpt)}
|
68
|
+
if (typeof Castanaut.result != 'undefined') {
|
69
|
+
document.title = Castanaut.result;
|
70
|
+
}
|
71
|
+
" in front document
|
72
|
+
set the_result to ((name of window 1) as string)
|
73
|
+
do JavaScript "
|
74
|
+
document.title = document.oldTitle;
|
75
|
+
" in front document
|
76
|
+
return the_result
|
77
|
+
end tell
|
78
|
+
`
|
79
|
+
end
|
80
|
+
|
81
|
+
def element_coordinates(selector, options = {})
|
82
|
+
index = options[:index] || 0
|
83
|
+
gebys = script('gebys.js')
|
84
|
+
cjs = script('coords.js')
|
85
|
+
coords = execute_javascript(%Q`
|
86
|
+
#{gebys}
|
87
|
+
#{cjs}
|
88
|
+
Castanaut.result = Castanaut.Coords.forElement(
|
89
|
+
'#{selector}',
|
90
|
+
#{index}
|
91
|
+
);
|
92
|
+
`)
|
93
|
+
|
94
|
+
unless coords.match(/\d+ \d+ \d+ \d+/)
|
95
|
+
raise Castanaut::Exceptions::ElementNotFound
|
96
|
+
end
|
97
|
+
|
98
|
+
coords = coords.split(' ').collect {|c| c.to_i}
|
99
|
+
|
100
|
+
if coords.any? {|c| c < 0 }
|
101
|
+
raise Castanaut::Exceptions::ElementOffScreen
|
102
|
+
end
|
103
|
+
|
104
|
+
coords
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
module Exceptions
|
111
|
+
# When getting an element's coordinates, this is raised if no element on
|
112
|
+
# the page matches the selector given.
|
113
|
+
class ElementNotFound < CastanautError
|
114
|
+
end
|
115
|
+
|
116
|
+
# When getting an element's coordinates, this is raised if the element
|
117
|
+
# is found, but cannot be shown on the screen. Normally, we automatically
|
118
|
+
# scroll to an element that is currently off-screen, but sometimes that
|
119
|
+
# might not be possible (such as if the element is display: none).
|
120
|
+
class ElementOffScreen < CastanautError
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|