Hello
The following script appears to work as text service when saved as Service workflow in Automator under 10.6.5.
In short, it shows a html dialogue which is indeed a html form (method=get) in webview, intercept the form submission and get the form data from url. This way it can realise easily customizable complex dialogue in (plain) AppleScript.
# Recipe
1) Open /Applications/Automator.app, choose "Service" template, drag the "Run AppleScript" action from the left pane to workflow editor pane and replace the code in template with the code listed below as "file_name_helper.applescript".
2) Set service properties in workflow editor as {Service receives selected [text] in [any application], Replace selected text [enabled]}
3) Save Service with name, e.g., "File Name Helper".
4) Open /Applications/Utilities/AppleScript Editor.app, copy the code listed below as "set_service_timeout.applescript", edit the service name, which is assigned to variable SERVICE_, to match the name saved in 3) plus .workflow extension - e.g., "File Name Helper.workflow"
5) Run the "set_service_timeout.applescript" and it will add NSTimeout = '600000' (600 sec) in Info.plist of the workflow. This is necessary because the default Info.plist created by Automator does not contain this key and thus the timeout is set to the default value 30000 millisec, which can be too short for this job.
6) Check whether the service is enabled in /Applications/System Preferencs > Keyboard > Keyboard Shortcuts > Services.
# Usage
Select editable text (in Save Panel etc), show its contextal menu by right-click and select, e.g., Services > File Name Helper. It will show a window to compose file name as requested. Complete it, press Done button and it will check its validity and if passed, replace the text in current selection with the composed value.
Currently the selection list items are dummy values. Edit the option elements in html to suit your needs.
# Notes
The script consists of two parts: one is rubycocoa code to process "html dialogue" and the other is html code to define the form. The "html dialogue" script returns the submitted form data as text representation of {name, value} pairs where name and value are separated by field separator (FS) and pairs are separated by record separator (RS). Both FS and RS are given as -p option of ruby script. The other options include -t (window title), -s (window size), -b (base url), -e (html source). It also accepts html file path or url as direct parameter. The html_dialogue() handler is a simple wrapper of this ruby script.
Form implements some features such as validation in javascript. Escape key acts as Cancel button but Return key does not acts as Done button (by design). Form element's name and value pairs, what ever they are, are retrieved by "html dialogue" script.
This rubycocoa script does not work well under 10.5 because of the lack of necessary method NSApplication's -setActivationPolicy(). Without this, ruby script run in shell cannot obtain keyboard focus.
* Every time you edit the Service workflow in Automator.app, you need to set the timeout value in Info.plist by editing the file (and reset pasteboard server) manually or running the set_service_timeout.applescript because Automator replaces the Info.plist file with the original.
Codes are moderately tested under OSX 10.6.5.
Hope this may help,
H
--file_name_helper.applescript
_main()
on _main()
script o
property rr : {}
property FS : character id 28 -- U+001C FIELD SEPARATOR
property RS : character id 30 -- U+001E RECORD SEPARATOR
(*
property FS : tab
property RS : linefeed
*)
set r to html_dialogue({|:title|:"File Name Helper", |:size|:{540, 300}, |:separators|:{FS, RS}, |:html|:_html()})
--return r
if r = "" then error number -128 -- user cancel (dialogue is closed)
set rr to _text2array(r, FS, RS)
repeat with r in my rr
set r to r's contents
if r's item 1 = "__f0__" then set n to r's item 2
if r's item 1 = "__fb__" then set b to r's item 2
end repeat
if b = "CANCEL" then error number -128 -- user cancel (cancel button is pressed)
return n
end script
tell o to run
end _main
on _text2array(t, cs, RS)
script o
property pp : _split(RS, t)
property qq : {}
repeat with p in my pp
set end of my qq to _split(cs, p's contents)
end repeat
return my qq's contents
end script
tell o to run
end _text2array
on _split(d, t)
(*
string or list d : separator(s)
string t : source string
return list : t splitted by d
*)
local astid, astid0, tt
set astid to a reference to AppleScript's text item delimiters
try
set {astid0, astid's contents} to {astid's contents, {} & d}
set tt to t's text items
set astid's contents to astid0
on error errs number errn
set astid's contents to astid0
error errs number errn
end try
return tt
end _split
on html_dialogue(argv)
(*
record argv : arguments record
|:title| : (string) window title
|:size| : (list) window size; {width, height}
|:separators| : (list) {field separator, record separator} of output
|:base| : (string) base URL
|:html| : (string) HTML source code
|:file| : (string) POSIX path or url of HTML file
v0.3
*)
set rb to "require 'osx/cocoa'
OSX.require_framework 'WebKit'
include OSX
class AppDelegate < NSObject
DEBUG = true
def initWithArguments(args)
@title, @size, @sepa, @base, @html, @file = args.values_at(:title, :size, :sepa, :base, :html, :file)
self
end
def applicationDidFinishLaunching(notif)
begin
@win = NSWindow.alloc.objc_send(
:initWithContentRect, NSMakeRect(0, 0, *@size),
:styleMask, NSTitledWindowMask | NSMiniaturizableWindowMask | NSClosableWindowMask, # | NSResizableWindowMask,
:backing, NSBackingStoreBuffered,
:defer, false)
@win.setLevel(NSStatusWindowLevel)
@win.setTitle(@title)
@win.center
webview = WebView.alloc.objc_send(
:initWithFrame, NSMakeRect(0, 0, *@size),
:frameName, nil,
:groupName, nil)
webview.setFrameLoadDelegate(self)
webview.setPolicyDelegate(self)
@win.setContentView(webview)
@win.orderFrontRegardless if DEBUG
if @html
webview.mainFrame.objc_send(
:loadHTMLString, @html,
:baseURL, @base ? NSURL.URLWithString(@base) : nil)
else
if ( @file =~ %r[^(https?|file)://]i )
url = NSURL.URLWithString(@file)
else
url = NSURL.fileURLWithPath(@file)
end
urlreq = NSURLRequest.requestWithURL(url)
webview.mainFrame.loadRequest(urlreq)
end
rescue
NSApp.terminate(self)
raise
end
end
def applicationShouldTerminateAfterLastWindowClosed(app)
true
end
def applicationShouldTerminate(sender)
NSTerminateNow
end
def webView_didFinishLoadForFrame(sender, frame)
return unless frame == sender.mainFrame # ignore other than main frame
# set NSApp's activation policy (10.6 or later only)
if NSApp.respondsToSelector?('setActivationPolicy_')
NSApp.setActivationPolicy(NSApplicationActivationPolicyRegular)
end
NSApp.activateIgnoringOtherApps(true)
@win.orderFrontRegardless
end
def webView_decidePolicyForNavigationAction_request_frame_decisionListener(sender, action, req, frame, listener)
# intercept form submittion
if action.objectForKey(WebActionNavigationTypeKey) == WebNavigationTypeFormSubmitted
url = action.objectForKey(WebActionOriginalURLKey)
print array2text(parse_form_url(url.to_s), *@sepa)
NSApp.terminate(self)
else
listener.use
end
end
end
def array2text(aa, fs, rs)
#
# array aa : source 2d array
# string fs : field separator
# string rs : record separator
# return string : text representation of 2d array
#
return ( aa.map {|a| a.join(fs)} ).join(rs)
end
def parse_form_url(u)
#
# string u : url including submitted form data as ?name1=value1&name2=value2 ...
# return array : array of [name, value] pairs in form data
#
# get form part following the last ? in url
f = u.sub(/ ^ .* \\? /ox, '')
# get [name, value] pairs
aa = f.scan(/ (.+?) = (.*?) (?:&|$) /ox)
# replace + with space in value and unescape value in [name, value] pairs
return aa.map do |x, y|
y_ = y.gsub(/\\+/, ' ').to_ns
[x, y_.stringByReplacingPercentEscapesUsingEncoding(NSUTF8StringEncoding).to_s]
end
end
def parse_options(argv)
require 'optparse'
args = {}
op = OptionParser.new do|o|
o.banner = %Q[Usage: #{File.basename($0)} [options] [file]]
# default values
args[:title] = 'Untitled' # dialogue window title
args[:base] = nil # base url
args[:size] = [600,600] # [width, height] of dialogue; unit = point
args[:sepa] = [%Q[\\t], %Q[\\n]] # Output separators for fields and records
args[:html] = nil # html text
o.on( '-t', '--title [NAME]', String, 'Window title.' ) do |t|
args[:title] = t
end
o.on( '-b', '--baseurl [URL]', String, 'Base URL.' ) do |u|
args[:base] = u
end
o.on( '-s', '--size [W,H]', Array, 'Dialogue width and height [point].' ) do |s|
raise OptionParser::InvalidArgument, %Q[#{s.join(',')}] unless s.length == 2
args[:size] = s.map {|x| x.to_f}
end
o.on( '-p', '--separators [FS,RS]', Array, 'Output field and record separators.' ) do |p|
raise OptionParser::InvalidArgument, %Q[#{p.join(',')}] unless p.length == 2
args[:sepa] = p
end
o.on( '-e', '--html [HTML]', String, 'HTML source code.' ) do |e|
args[:html] = e
end
o.on( '-h', '--help', 'Display this help.' ) do
puts o
exit 1
end
end
begin
op.parse!(argv)
rescue => ex
puts %Q[# #{ex.class} : #{ex.message}]
puts op.help()
exit 1
end
unless args[:html] || argv.length == 1
puts op.help()
exit 1
end
args[:file], = argv.length > 0 ? argv : nil
$stderr.puts 'Both html text and file are given. File is ignored.' if args[:file] && args[:html]
args
end
def main(argv)
args = parse_options(argv)
app = NSApplication.sharedApplication
delegate = AppDelegate.alloc.initWithArguments(args)
app.setDelegate(delegate)
app.run
end
if __FILE__ == $0
main(ARGV)
end
"
set defaults to {|:title|:"Untitled", |:size|:{600, 600}, |:separators|:{tab, linefeed}, |:base|:"", |:html|:"", |:file|:""}
set argv to argv & defaults
set {|:title|:t, |:size|:{w, h}, |:separators|:{FS, RS}, |:base|:b, |:html|:e, |:file|:f} to argv
set args to " -t " & t's quoted form & ¬
" -s " & ("" & w & "," & h)'s quoted form & ¬
" -p " & (FS & "," & RS)'s quoted form
if b ≠ "" then set args to args & " -b " & b's quoted form
if e ≠ "" then set args to args & " -e " & e's quoted form
if f ≠ "" then set args to args & " " & f's quoted form
do shell script "/usr/bin/ruby -e " & rb's quoted form & " -- " & args
end html_dialogue
on _html()
"<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN' 'http://www.w3.org/TR/html4/loose.dtd'>
<html>
<head>
<meta http-equiv='content-type' content='text/html; charset=utf-8'>
<style type='text/css'>
* { font: normal 10pt/14pt 'Lucida Grande'; }
body { background-color: rgb(230, 240, 230); }
form { margin: 12px; }
label { display: inline-block; width: 100px; text-align: right; padding: 6px; }
option { padding-left: 6px; }
textarea { resize: none; }
.multicol { -moz-column-count: 2; -webkit-column-count: 2; } /* CSS3 */
.field { display: inline-block; width: 240px; }
.buttons { display: block; padding: 6px; }
.const { display: inline-block; width: 24px; text-align: right; }
.left { float: left; }
.right { float: right; }
.error { color: rgb(200, 10, 10);}
.normal { color: inherit; }
#status1 { padding: 2px 6px; }
</style>
</head>
<body onkeydown='return _keydown(event)'>
<form id='form1' action='form.get' method='get' onsubmit='return _validate(this)'>
<fieldset>
<legend>Compose File Name</legend>
<div class='multicol'>
<span class='field'>
<label for='__f1__'>Date:</label>
<input type='text' name='__f1__' size=8 value='' readonly tabindex=-1>
</span>
<span class='field'>
<label for='__f2__'>Client:</label>
<select name='__f2__' onchange='_update()' tabindex=2>
<option value='?' selected>- select -</option>
<option value='A1'>A1</option>
<option value='A2'>A2</option>
<option value='A3'>A3</option>
<option value='A4'>A4</option>
</select>
</span>
<span class='field'>
<label for='__f3__'>Project:</label>
<select name='__f3__' onchange='_update()' tabindex=3>
<option value='?' selected>- select -</option>
<option value='B1'>B1</option>
<option value='B2'>B2</option>
<option value='B3'>B3</option>
<option value='B4'>B4</option>
</select>
</span>
<span class='field'>
<label for='__f4__'>Sub Project:</label>
<select name='__f4__' onchange='_update()' tabindex=4>
<option value='?' selected>- select -</option>
<option value='C1'>C1</option>
<option value='C2'>C2</option>
<option value='C3'>C3</option>
<option value='C4'>C4</option>
</select>
</span>
<span class='field'>
<label for='__f5__'>Part:</label>
<select name='__f5__' onchange='_update()' tabindex=5>
<option value='?' selected>- select -</option>
<option value='D1'>D1</option>
<option value='D2'>D2</option>
<option value='D3'>D3</option>
<option value='D4'>D4</option>
</select>
</span>
<span class='field'>
<label for='__f6__'>Application:</label>
<select name='__f6__' onchange='_update()' tabindex=6>
<option value='?' selected>- select -</option>
<option value='E1'>E1</option>
<option value='E2'>E2</option>
<option value='E3'>E3</option>
<option value='E4'>E4</option>
</select>
</span>
<span class='field'>
<label for='__f7__'>Version (#.#):</label>
<span class='const'>V</span>
<input type='text' name='__f7__' size=5 maxlength=5 value='#.#' onchange='_update()' tabindex=7>
</span>
<span class='field'>
<label for='__f8__'>Editor (#):</label>
<span class='const'>AES</span>
<input type='text' name='__f8__' size=5 maxlength=5 value='#' onchange='_update()' tabindex=8>
</span>
</div>
<hr>
<!-- aggregated string -->
<div id='status1' class='normal'> </div>
<textarea name='__f0__' rows=2 cols=55 readonly tabindex=-1></textarea>
<span class='buttons'>
<!-- visible control buttons -->
<button type='button' onclick='_reset()' class='left' tabindex=102>Reset</button>
<button type='button' onclick='_submit(this)' class='right' name='__fb1__' value='DONE' tabindex=100>Done</button>
<button type='button' onclick='_submit(this)' class='right' name='__fb2__' value='CANCEL' tabindex=101>Cancel</button>
<!-- hidden submit button whose value is set in _submit() -->
<button type='submit' style='display: none' name='__fb__'></button>
</span>
</fieldset>
</form>
<script type='text/javascript'>
// set global variables and reset the form
var form1 = document.getElementById('form1');
var status1 = document.getElementById('status1');
var str1 = 'Current value';
var str2 = 'Value is invalid. Please revise the selection.';
_reset();
// set onkeypress event handler for input[type=text] element so as to suppress form submission by return key
var aa = document.getElementsByTagName('input');
for ( i = 0; i < aa.length; ++i )
{
if ( aa[i].type == 'text' ) { aa[i].setAttribute('onkeypress', 'return _keypress(event)'); }
}
// intercept return key to suppress form submission
function _keypress(e)
// event e : keypress event to be handled
{
if ( e.which == 13 ) { _update(); return false; }
return true;
}
// intercept escape key to cancel the form
function _keydown(e)
// event e : keypress event to be handled
{
if ( e.which == 27 ) { form1.__fb2__.click(); return false; }
return true;
}
// validate data
function _validate(f)
// form element f : target form
{
if ( f.__fb__.value == 'CANCEL' ) { return true; }
if ( f.__f1__.value == '?' ) { f.__f1__.focus(); _status(str2, 'error'); return false; }
if ( f.__f2__.value == '?' ) { f.__f2__.focus(); _status(str2, 'error'); return false; }
if ( f.__f3__.value == '?' ) { f.__f3__.focus(); _status(str2, 'error'); return false; }
if ( f.__f4__.value == '?' ) { f.__f4__.focus(); _status(str2, 'error'); return false; }
if ( f.__f5__.value == '?' ) { f.__f5__.focus(); _status(str2, 'error'); return false; }
if ( f.__f6__.value == '?' ) { f.__f6__.focus(); _status(str2, 'error'); return false; }
if ( ! f.__f7__.value.match(/^\\d+\\.\\d+$/) ) { f.__f7__.focus(); f.__f7__.select(); _status(str2, 'error'); return false; }
if ( ! f.__f8__.value.match(/^\\d+$/) ) { f.__f8__.focus(); f.__f8__.select(); _status(str2, 'error'); return false; }
return true;
}
// show status message
function _status(t, c)
{
status1.firstChild.nodeValue = t;
status1.setAttribute('class', c);
}
// get date stamp in YYYYMMDD format
function _datestamp()
{
var d = new Date();
return '' + d.getFullYear() + ('0' + (d.getMonth() + 1)).substr(-2, 2) + ('0' + d.getDate()).substr(-2, 2);
}
// update the aggregated string
function _update()
{
_status(str1, 'normal');
form1.__f1__.value = _datestamp();
form1.__f0__.value = ''
+ form1.__f1__.value + '_' // date
+ form1.__f2__.value + '_' // client
+ form1.__f3__.value + '_' // project
+ form1.__f4__.value + '_' // sub project
+ form1.__f5__.value + '_' // part
+ form1.__f6__.value + '_' // application
+ 'V'
+ form1.__f7__.value + '_' // version
+ 'AES'
+ form1.__f8__.value; // editor
}
// reset form
function _reset()
{
form1.reset(); _update();
}
// submit form
function _submit(b)
// button element b : visible button clicked to submit the form
{
form1.__fb__.value = b.value;
form1.__fb__.click();
}
</script>
</body>
</html>"
end _html
--set_service_timeout.applescript
(*
Set sevice's timeout value
*)
set SERVICE_ to "File Name Helper.workflow" -- file name of service in ~/Library/Services
set TIMEOUT_ to "600000" -- 600 sec
set sh to "
SERVICE=" & SERVICE_'s quoted form & "
TIMEOUT=" & TIMEOUT_'s quoted form & "
# set service's timeout in Info.plist
f=~/Library/Services/\"${SERVICE}\"/Contents/Info.plist
/usr/libexec/plistbuddy -c 'Set :NSServices:0:NSTimeout '$TIMEOUT \"$f\" || \\
/usr/libexec/plistbuddy -c 'Add :NSServices:0:NSTimeout string '$TIMEOUT \"$f\"
/usr/libexec/plistbuddy -c 'Print :NSServices:0:NSTimeout' \"$f\"
# reset pasteboard server (in 10.6 or later)
/System/Library/CoreServices/pbs -flush
"
set r to do shell script sh
if r = TIMEOUT_ then
display dialog "Done."
else
display dialog "Failed to set timeout."
end if
Message was edited by: Hiroto (fixed typos)