castanaut 1.0.0
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/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
|