I asked this in Applescript forum but thought someone might know of another way to accomplish the same end result.

I asked this in Applescript forum but thought someone might know of another way to accomplish the same end result.

Need to know to if this is even possible using a text expansion application or script or anything else.


I've been trying to figure out a way to name a file in any application in the Save dialog but I want it to follow a specific format that uses multiple list menus based on input from other list menus


I've used text expansion applications before (Keyboard Meastro, Typinator, aText, Dash, iKey, yType) for some simple task and some not so simple task but this formating might be too big of a request for those types of programs.

I would love to be wrong on that assumption, so please feel free to correct me if you know otherwise.



Desired format is:

Today'sDate_ClientCode_ProjectCode_SubProject_Part_AppUsed_Version_EditorCode



Example would look like this:

20130423_A1_BON_ShowA_Finale_LP_V1.4_AES1


This would be a simple request if everyone could remember all the codes but the list is large and at times also dependant on the previous code in the sequence.



Breakdown (in most lists, the # of list menu items may expand)


RED = User input by typing

GREEN = Auto typed with no input from user

BLUE = Auto typed after user selects item from list


Type the shortcut "?ses" to start expansion or script


Date - Should auto type today's date and follow the yyyyMMdd format, then auto type "_" then open the "Client Code" list menu

20130423_"Client list menu"


Client - Choose from a list menu of say 10 options and that choice would auto type a 2 letter code and "_" then open the "Project Code" list menu that corresponds with that "Code" (example A1)

20130423_A1_"A1 Project list menu"


Project - Choose from that corresponding list menu and auto type it's code and "_" then open the SubProject list menu that corresponds with that "Code" (example BON)


20130423_A1_BON_"BON SubProject list menu"


SubProject - Choose from corresponding list menu and auto type it's code and "_" then open the "Part" list menu (list is independent of previous choice)


20130423_A1_BON_ShowA_"Part list menu"


Part - Choose from "Part list menu" and auto type the choice and "_" then open the "Application Used" list menu (list is independent of previous choice)


20130423_A1_BON_ShowA_Finale_"Application Usedlist menu"


AppUsed - Choose from list menu and auto type two letter code and "_" then auto type the letter "V"

20130423_A1_BON_ShowA_Finale_LP_V"User Input"


Version - User input but should follow "#.#" such as 1.2 or 2.4, user hits enter when done and auto types "AES"

20130423_A1_BON_ShowA_Finale_LP_V1.4_AES"User Input"


UserCode - User inputs number and hits enter.


20130423_A1_BON_ShowA_Finale_LP_V1.4_AES1


Move cursor to the end of name


This completes the naming process but does not close the original Save menu so the user can review the new name first





Thanks in advanced.

OS X Mountain Lion (10.8.3)

Posted on Apr 24, 2013 1:23 PM

Reply
23 replies

May 2, 2013 12:53 AM in response to Square1

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)

May 2, 2013 1:26 PM in response to red_menace

Hello red_menace,


In RubyCocoa, there's a dedicated function objc_method() to let the Objective-C runtime know the method signature. In implementing a service method, you need to code something like this -


def serviceMethod_userData_error(pboard, udata, error)
    # service definition here
end
objc_method(:serviceMethod_userData_error, 'v@:@@^@')


I don't know about MacRuby but I'd expect similar function is provided as well. AppleScriptObjC... I don't know.


As a working example, here's a service written in RubyCocoa -


Custom Styles in TextEdit - Background Highlighting

https://discussions.apple.com/thread/4472812?start=0&tstart=0


Kind regards,

H

May 2, 2013 1:46 PM in response to Hiroto

All the examples I've been able to find more-or-less follow the Cocoa documentation, but it looks like the runtime is trying to call a method using the colon separators. In both AppleScriptObjC and MacRuby, I can set the class, but the method just wasn't being found, so I'm guessing something changed since those articles were written.


I wound up going the other way - using an Objective-C wrapper for the service method that calls the AppleScriptObjC class. I don't really care that much for Objective-C, but fortunately there wasn't that much to do.

May 2, 2013 1:52 PM in response to Hiroto

Hiroto,


Red_menace's MetaRenamer app works quite well and your example worked well also but as of now, the only thing that would prevent me from using either one is the need for menu items to change based on the previous menu selection. The "Project" and "SubProject" menus would have too many choices if they were just a long lists.



Example


If the Client selection is Marketing

then Project Menu is Marketing Projects


If Project selection is Christmas

then SubProject Menu is Christmas SubProjects


If SubProject selection is SantasShow

then Parts Menu is SantasShow Parts





Second example



If the Client selection is PublicRelations

then Project Menu is PublicRelations Projects


If Project selection is Presentations

then SubProject Menu is Presentations SubProjects


If SubProject selection is GovernmentAndYou

then Parts Menu is GovernmentAndYou Parts





Another example


Client would have 1 Client menu with "x" number of options


If the option chosen was item A3 in the Client menu

then Project menu would switch to A3 Project Items with "x" number of choices


But if the option chosen was A2 in the Client menu

then Project menu would switch to A2 Project Items with "x" number of choices




The parts menu in the first two examples might actual end up being the same menu for now but may need to grow in the future.


Most of what you and Red_menace are doing is above me for now but hopefully I will continue to learn with help from people like you, red and frank. Thanks for the help.

May 3, 2013 2:58 PM in response to Square1

OK. That's new requirment. It can be done by designing the form as such.


Replace the _html() handler in the previous script with the code listed below, which consists of 4 handlers.


Specify the every possible combination of 4 options in the from of "option path", i.e., /client/project/sub-project/part, in the _option_paths() handler. Also specify the name of applications in the _app_names() handler. Each entry must be on its own line and line must be terminated by linefeed character. Javascript in html will build the selection list dynamically according to these data.


Number of option paths could be very large. Depth-4 path with 10 nodes per level yields 10000 paths. I'm not sure the script can operate at acceptable speed with such amount of data.


The following shell script can help to generate the option path list on desktop provided that you create file system directory tree rooted at ~/desktop/options which matches the option paths. E.g. ~/desktop/options/client1/project1/subproject1/part1.


#!/bin/bash

cd ~/desktop/options
find . -type d -mindepth 4 -maxdepth 4 | awk '{print substr($0, 2)}' > ~/desktop/option_path_list.txt


Good luck,

H




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>Testing</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>
                        </select>
                    </span>
                    <span class='field'>
                        <label for='__f3__'>Project:</label>
                        <select name='__f3__' onchange='_update()' tabindex=3>
                            <option value='?' selected>- select -</option>
                        </select>
                    </span>
                    <span class='field'>
                        <label for='__f4__'>Sub Project:</label>
                        <select name='__f4__' onchange='_update()' tabindex=4>
                            <option value='?' selected>- select -</option>
                        </select>
                    </span>
                    <span class='field'>
                        <label for='__f5__'>Part:</label>
                        <select name='__f5__' onchange='_update()' tabindex=5>
                            <option value='?' selected>- select -</option>
                        </select>
                    </span>
                    <span class='field'>
                        <label for='__f6__'>Application:</label>
                        <select name='__f6__' onchange='_update()' tabindex=6>
                            <option value='?' selected>- select -</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.';
            var opaths = _option_paths();
            var oapps = _app_names();
            _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)
            //     string t : status text
            //     string c : class name of status span element
            {
                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 form
            function _update()
            {
                _status(str1, 'normal');
                form1.__f1__.value = _datestamp();
                
                var n;
                var p = '';
                if ( (n = form1.__f2__.value) == '?' )
                    _build_options(form1.__f2__, p, opaths);
                else
                    p += '/' + n;

                if ( node_depth(p) < 1 )
                    _build_options(form1.__f3__, '', '');
                else if ( (n = form1.__f3__.value) == '?' || child_nodes_at_path(p, opaths).indexOf(n) < 0 )
                    _build_options(form1.__f3__, p, opaths);
                else
                    p += '/' + n;
                
                if ( node_depth(p) < 2 )
                    _build_options(form1.__f4__, '', '');
                else if ( (n = form1.__f4__.value) == '?' || child_nodes_at_path(p, opaths).indexOf(n) < 0 )
                    _build_options(form1.__f4__, p, opaths);
                else
                    p += '/' + n;

                if ( node_depth(p) < 3 )
                    _build_options(form1.__f5__, '', '');
                else if ( (n = form1.__f5__.value) == '?' || child_nodes_at_path(p, opaths).indexOf(n) < 0 )
                    _build_options(form1.__f5__, p, opaths);
                else
                    p += '/' + n;
                
                if ( form1.__f6__.value == '?' )
                    _build_options(form1.__f6__, '', oapps);
                
                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();
            }


            // get child nodes of the node at specified path
            function child_nodes_at_path(p, t)
            // string p : path of the node in opaths
            // string t : options paths (LF delimited paths)
            {
                var re = RegExp('^' + _escape_metachars(p) + '/?([^/\\n]+)', 'gm');
                var rr = [];
                while ( (aa = re.exec(t)) !== null )
                {
                    var a = aa[1];
                    rr[rr.length - 1] !== a && rr.push(a);
                }
                return rr;
            }
            
            // get node depth
            function node_depth(p)
            // string p : path
            {
                var aa = p.match( /\\/./g );
                return aa === null ? 0 : aa.length;
            }
                
            //
            function _build_options(s, p, t)
            // select element : s
            // string p : directory path of the options node
            // string t : options paths (LF delimited paths)
            {
                while ( s.getElementsByTagName('option').length > 1 ) { s.removeChild(s.lastChild); }
                var aa = child_nodes_at_path(p, t);
                for ( var i = 0; i < aa.length; ++i )
                {
                    var option = document.createElement('option');
                    option.setAttribute('value', aa[i]);
                    option.appendChild(document.createTextNode(aa[i]));
                    s.appendChild(option);
                }
            }

            // escape regexp meta characters
            function _escape_metachars(t)
            // string t : source string
            {
                return t.replace( /[.+*?^$|[{(\\\\]/g, '\\\\$&' );
            }

            // return text of sorted list of comprehensive paths of options ( /client/project/sub-project/part )
            function _option_paths()
            {
                return " & _quoted_text_for_js(_option_paths(), {|:sort|:-1}) & ";
            }
            
            // return name of applications
            function _app_names()
            {
                return " & _quoted_text_for_js(_app_names(), {|:sort|:0}) & ";
            }
            
        </script>
    </body>
</html>
"
end _html

on _quoted_text_for_js(t, {|:sort|:srt})
    (*
        string t : source string
        integer srt : sort direction; -1 = ascending, 1 = descending, 0 = no sort
        return string : single quoted string for javascript
    *)
    if srt < 0 then
        set _sort_ to ".sort."
    else if srt = 0 then
        set _sort_ to "."
    else if srt > 0 then
        set _sort_ to ".sort { |a, b| b <=> a }."
    end if
    set rb to "puts %['] << ARGF.readlines" & _sort_ & "map {|a| a.chomp }.
join(%[\\x00]).gsub(/['\\\\]/) { %[\\\\] + $& }.gsub(/\\x00/, '\\n') << %[']"
    return do shell script "cat <<'__EOF__' | /usr/bin/ruby -e " & rb's quoted form & "
" & t & "
__EOF__"
end _quoted_text_for_js

on _option_paths()
    return "
/A1/A11/A111/A1111
/A1/A11/A111/A1112
/A1/A11/A112/A1121
/A1/A11/A112/A1122
/A1/A12/A121/A1211
/A1/A12/A121/A1212
/A1/A12/A122/A1221
/A1/A12/A122/A1222
/A2/A21/A211/A2111
/A2/A21/A211/A2112
/A2/A21/A212/A2121
/A2/A21/A212/A2122
/A2/A22/A221/A2211
/A2/A22/A221/A2212
/A2/A22/A222/A2221
/A2/A22/A222/A2222
/B1/B11/B111/B1111
/B1/B11/B111/B1112
/B1/B11/B112/B1121
/B1/B11/B112/B1122
/B1/B12/B121/B1211
/B1/B12/B121/B1212
/B1/B12/B122/B1221
/B1/B12/B122/B1222
/B2/B21/B211/B2111
/B2/B21/B211/B2112
/B2/B21/B212/B2121
/B2/B21/B212/B2122
/B2/B22/B221/B2211
/B2/B22/B221/B2212
/B2/B22/B222/B2221
/B2/B22/B222/B2222
/C1/C11/C111/C1111
/C1/C11/C111/C1112
/C1/C11/C112/C1121
/C1/C11/C112/C1122
/C1/C12/C121/C1211
/C1/C12/C121/C1212
/C1/C12/C122/C1221
/C1/C12/C122/C1222
/C2/C21/C211/C2111
/C2/C21/C211/C2112
/C2/C21/C212/C2121
/C2/C21/C212/C2122
/C2/C22/C221/C2211
/C2/C22/C221/C2212
/C2/C22/C222/C2221
/C2/C22/C222/C2222
/D1/D11/D111/D1111
/D1/D11/D111/D1112
/D1/D11/D112/D1121
/D1/D11/D112/D1122
/D1/D12/D121/D1211
/D1/D12/D121/D1212
/D1/D12/D122/D1221
/D1/D12/D122/D1222
/D2/D21/D211/D2111
/D2/D21/D211/D2112
/D2/D21/D212/D2121
/D2/D21/D212/D2122
/D2/D22/D221/D2211
/D2/D22/D221/D2212
/D2/D22/D222/D2221
/D2/D22/D222/D2222
"
end _option_paths

on _app_names()
    return "
E1
E2
E3
E4
"
end _app_names

May 4, 2013 11:35 AM in response to Square1

Hello


After re-reading your descriptions and sample images, I revised the script as follows. It now handles menu item of abbreviation and long item description such as "MR = Marketing/Public Relations". It will take the first chunk of non-space characters in menu item as abbreviation code and use it in file name composition.


I separated the script into two: 1) html dialogue script to be saved as service (SCRIPT 1 below) and 2) html generator script to create html form (SCRIPT 2 below), mainly because in the previous form it will eventually fail by exceeding the limit of argument length of do shell script command when the html size grows with large option paths given inline. In new scheme, html dialogue script will read html file saved on disk instead of use html source given inline. Also in this scheme, you need not to edit service workflow every time you edit the menu items.


The html generator script retrieves the option paths from directory tree in file system. So you need to prepare file system directory tree (hereinafter "option tree") which represents the option paths. Currently the script assumes that option tree resides in ~/desktop/test/options, e.g. ~/desktop/test/options/client1/project1/subproject1/part1 etc. You can change this path setting in properties in SCRIPT 2. Here I include SCRIPT 3 which generates a test option tree in ~/desktop/test/options.


After testing, you may change the script properties master_dir and html_path in SCRIPT 1 and properties master_dir, html_path and option_tree_root in SCRIPT 2 to suit your need. Note that property html_path in SCRIPT 1 & 2 must be the same value.


Application names are to be given in _app_names() handler in SCRIPT 2.


It was not clear to me whether the Part is to be assumed independent of the previous menu choice and so I assumed it is dependent on the previous choice and put it in option path. It is possible to make it independent and remove it from option path.


Hope this may help,

H


PS. I will be busy this week and not be able to respond in this discussion. Maybe until next weekend. Good luck.




--SCRIPT 1
(*
    main script to handle html dialogue
*)
property master_dir : (path to desktop)'s POSIX path & "test" -- e.g., /Users/USERNAME/Desktop/test
property html_path : master_dir & "/form.html"

_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|:{700, 420}, |:separators|:{fs, rs}, |:file|:html_path})
        --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 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) HTML file POSIX path or url

            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
--END OF SCRIPT 1



--SCRIPT 2
(*
    script to generate form html
*)
(*
    part 1 : user defined propreties and handler
*)
property master_dir : (path to desktop)'s POSIX path & "test" -- /Users/USERNAME/Desktop/test
property option_tree_root : master_dir & "/options"
property html_path : master_dir & "/form.html"

on _app_names()
    return "
E1
E2
E3
E4
"
end _app_names


(*
    part 2 : predefined script
*)
write_to_file(_html(), html_path, {_append:false, _class:«class utf8»})
display dialog "Form html is generated in " & return & html_path

on _option_paths(root)
    (*
        string root : POSIX path of root directory of option tree
    *)
    set sh to "
cd " & root's quoted form & " || exit 1
find . -type d -mindepth 4 -maxdepth 4 | awk '{print substr($0, 2)}'
"
    do shell script sh without altering line endings
end _option_paths

on _quoted_text_for_js(t, {|:sort|:srt})
    (*
        string t : source string
        integer srt : sort direction; -1 = ascending, 1 = descending, 0 = no sort
        return string : single quoted string for javascript
    *)
    if srt < 0 then
        set _sort_ to ".sort."
    else if srt = 0 then
        set _sort_ to "."
    else if srt > 0 then
        set _sort_ to ".sort { |a, b| b <=> a }."
    end if
    set rb to "puts %['] << ARGF.readlines" & _sort_ & "map {|a| a.chomp }.
join(%[\\x00]).gsub(/['\\\\]/) { %[\\\\] + $& }.gsub(/\\x00/, '\\n') << %[']"
    return do shell script "cat <<'__EOF__' | /usr/bin/ruby -e " & rb's quoted form & "
" & t & "
__EOF__"
end _quoted_text_for_js

on write_to_file(x, p, {_append:_append, _class:_class})
    (*
        data x : anything to be written to output file
        string p : POSIX path of output file
        boolean _append: true to append data, false to replace data
        type class _class: type class as which the data is written
    *)
    local fh
    try
        set fh to open for access (POSIX file p) with write permission
        if not _append then set eof fh to 0
        write x as _class to fh starting at eof
        close access fh
    on error errs number errn
        try
            close access (POSIX file p)
        on error --
        end try
        error "write_to_file(): " & errs number errn
    end try
end write_to_file



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: 1; -webkit-column-count: 1; }    /* CSS3 */
            .field { display: inline-block; width: 600px; }
            .buttons { display: block; padding: 6px; }
            .const { display: inline-block; width: 24px; text-align: right; }
            .left { float: left; }
            .right { float: right; }
            .error { color: rgb(230, 50, 50);}
            .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>
                        </select>
                    </span>
                    <span class='field'>
                        <label for='__f3__'>Project:</label>
                        <select name='__f3__' onchange='_update()' tabindex=3>
                            <option value='?' selected>- select -</option>
                        </select>
                    </span>
                    <span class='field'>
                        <label for='__f4__'>Sub Project:</label>
                        <select name='__f4__' onchange='_update()' tabindex=4>
                            <option value='?' selected>- select -</option>
                        </select>
                    </span>
                    <span class='field'>
                        <label for='__f5__'>Part:</label>
                        <select name='__f5__' onchange='_update()' tabindex=5>
                            <option value='?' selected>- select -</option>
                        </select>
                    </span>
                    <span class='field'>
                        <label for='__f6__'>Application:</label>
                        <select name='__f6__' onchange='_update()' tabindex=6>
                            <option value='?' selected>- select -</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=70 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.';
            var opaths = _option_paths();
            var oapps = _app_names();
            _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)
            //     string t : status text
            //     string c : class name of status span element
            {
                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 form
            function _update()
            {
                _status(str1, 'normal');
                form1.__f1__.value = _datestamp();

                var n;
                var p = '';
                if ( (n = form1.__f2__.value) == '?' )
                    _build_options(form1.__f2__, p, opaths);
                else
                    p += '/' + n;

                if ( _node_depth(p) < 1 )
                    _build_options(form1.__f3__, '', '');
                else if ( (n = form1.__f3__.value) == '?' || _child_nodes_at_path(p, opaths).indexOf(n) < 0 )
                    _build_options(form1.__f3__, p, opaths);
                else
                    p += '/' + n;

                if ( _node_depth(p) < 2 )
                    _build_options(form1.__f4__, '', '');
                else if ( (n = form1.__f4__.value) == '?' || _child_nodes_at_path(p, opaths).indexOf(n) < 0 )
                    _build_options(form1.__f4__, p, opaths);
                else
                    p += '/' + n;

                if ( _node_depth(p) < 3 )
                    _build_options(form1.__f5__, '', '');
                else if ( (n = form1.__f5__.value) == '?' || _child_nodes_at_path(p, opaths).indexOf(n) < 0 )
                    _build_options(form1.__f5__, p, opaths);
                else
                    p += '/' + n;

                if ( form1.__f6__.value == '?' )
                    _build_options(form1.__f6__, '', oapps);

                form1.__f0__.value = '' 
                    + form1.__f1__.value + '_'            // date
                    + _word1(form1.__f2__.value) + '_'    // client (abbrev. only)
                    + _word1(form1.__f3__.value) + '_'    // project (abbrev. only)
                    + _word1(form1.__f4__.value) + '_'    // sub project (abbrev. only)
                    + _word1(form1.__f5__.value) + '_'    // part (abbrev. only)
                    + 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();
            }


            // get child nodes of the node at specified path
            function _child_nodes_at_path(p, t)
            // string p : path of the node in opaths
            // string t : options paths (LF delimited paths)
            {
                var re = RegExp('^' + _escape_metachars(p) + '/?([^/\\n]+)', 'gm');
                var rr = [];
                while ( (aa = re.exec(t)) !== null )
                {
                    var a = aa[1];
                    rr[rr.length - 1] !== a && rr.push(a);
                }
                return rr;
            }

            // get node depth
            function _node_depth(p)
            // string p : path
            {
                var aa = p.match( /\\/./g );
                return aa === null ? 0 : aa.length;
            }

            // get first word (first chunk of non-space characters)
            function _word1(t)
            // string t : source string
            {
                return t.match(/^\\s*(\\S*)/)[1];    // ignore leading spaces
            }

            //
            function _build_options(s, p, t)
            // select element : s
            // string p : directory path of the options node
            // string t : options paths (LF delimited paths)
            {
                while ( s.getElementsByTagName('option').length > 1 ) { s.removeChild(s.lastChild); }
                var aa = _child_nodes_at_path(p, t);
                for ( var i = 0; i < aa.length; ++i )
                {
                    var option = document.createElement('option');
                    option.setAttribute('value', aa[i]);
                    option.appendChild(document.createTextNode(aa[i]));
                    s.appendChild(option);
                }
            }

            // escape regexp meta characters
            function _escape_metachars(t)
            // string t : source string
            {
                return t.replace( /[.+*?^$|[{(\\\\]/g, '\\\\$&' );
            }

            // return text of sorted list of comprehensive paths of options ( /client/project/sub-project/part )
            function _option_paths()
            {
                return " & _quoted_text_for_js(_option_paths(option_tree_root), {|:sort|:-1}) & ";
            }

            // return name of applications
            function _app_names()
            {
                return " & _quoted_text_for_js(_app_names(), {|:sort|:0}) & ";
            }

        </script>
    </body>
</html>
"
end _html
--END OF SCRIPT 2



--SCRIPT 3
(*
    script to generate option tree for test
*)
do shell script "dir=~/desktop/test/options
mkdir -p \"$dir\"
cd \"$dir\" || exit 1

cat <<'EOF' | while read f;
/A1 = description/A11 = description/A111 = description/A1111 = description
/A1 = description/A11 = description/A111 = description/A1112 = description
/A1 = description/A11 = description/A112 = description/A1121 = description
/A1 = description/A11 = description/A112 = description/A1122 = description
/A1 = description/A12 = description/A121 = description/A1211 = description
/A1 = description/A12 = description/A121 = description/A1212 = description
/A1 = description/A12 = description/A122 = description/A1221 = description
/A1 = description/A12 = description/A122 = description/A1222 = description
/A2 = description/A21 = description/A211 = description/A2111 = description
/A2 = description/A21 = description/A211 = description/A2112 = description
/A2 = description/A21 = description/A212 = description/A2121 = description
/A2 = description/A21 = description/A212 = description/A2122 = description
/A2 = description/A22 = description/A221 = description/A2211 = description
/A2 = description/A22 = description/A221 = description/A2212 = description
/A2 = description/A22 = description/A222 = description/A2221 = description
/A2 = description/A22 = description/A222 = description/A2222 = description
/B1 = description/B11 = description/B111 = description/B1111 = description
/B1 = description/B11 = description/B111 = description/B1112 = description
/B1 = description/B11 = description/B112 = description/B1121 = description
/B1 = description/B11 = description/B112 = description/B1122 = description
/B1 = description/B12 = description/B121 = description/B1211 = description
/B1 = description/B12 = description/B121 = description/B1212 = description
/B1 = description/B12 = description/B122 = description/B1221 = description
/B1 = description/B12 = description/B122 = description/B1222 = description
/B2 = description/B21 = description/B211 = description/B2111 = description
/B2 = description/B21 = description/B211 = description/B2112 = description
/B2 = description/B21 = description/B212 = description/B2121 = description
/B2 = description/B21 = description/B212 = description/B2122 = description
/B2 = description/B22 = description/B221 = description/B2211 = description
/B2 = description/B22 = description/B221 = description/B2212 = description
/B2 = description/B22 = description/B222 = description/B2221 = description
/B2 = description/B22 = description/B222 = description/B2222 = description
/C1 = description/C11 = description/C111 = description/C1111 = description
/C1 = description/C11 = description/C111 = description/C1112 = description
/C1 = description/C11 = description/C112 = description/C1121 = description
/C1 = description/C11 = description/C112 = description/C1122 = description
/C1 = description/C12 = description/C121 = description/C1211 = description
/C1 = description/C12 = description/C121 = description/C1212 = description
/C1 = description/C12 = description/C122 = description/C1221 = description
/C1 = description/C12 = description/C122 = description/C1222 = description
/C2 = description/C21 = description/C211 = description/C2111 = description
/C2 = description/C21 = description/C211 = description/C2112 = description
/C2 = description/C21 = description/C212 = description/C2121 = description
/C2 = description/C21 = description/C212 = description/C2122 = description
/C2 = description/C22 = description/C221 = description/C2211 = description
/C2 = description/C22 = description/C221 = description/C2212 = description
/C2 = description/C22 = description/C222 = description/C2221 = description
/C2 = description/C22 = description/C222 = description/C2222 = description
/D1 = description/D11 = description/D111 = description/D1111 = description
/D1 = description/D11 = description/D111 = description/D1112 = description
/D1 = description/D11 = description/D112 = description/D1121 = description
/D1 = description/D11 = description/D112 = description/D1122 = description
/D1 = description/D12 = description/D121 = description/D1211 = description
/D1 = description/D12 = description/D121 = description/D1212 = description
/D1 = description/D12 = description/D122 = description/D1221 = description
/D1 = description/D12 = description/D122 = description/D1222 = description
/D2 = description/D21 = description/D211 = description/D2111 = description
/D2 = description/D21 = description/D211 = description/D2112 = description
/D2 = description/D21 = description/D212 = description/D2121 = description
/D2 = description/D21 = description/D212 = description/D2122 = description
/D2 = description/D22 = description/D221 = description/D2211 = description
/D2 = description/D22 = description/D221 = description/D2212 = description
/D2 = description/D22 = description/D222 = description/D2221 = description
/D2 = description/D22 = description/D222 = description/D2222 = description
EOF
do
    mkdir -p .\"$f\"
done
"
--END OF SCRIPT 3

This thread has been closed by the system or the community team. You may vote for any posts you find helpful, or search the Community for additional answers.

I asked this in Applescript forum but thought someone might know of another way to accomplish the same end result.

Welcome to Apple Support Community
A forum where Apple customers help each other with their products. Get started with your Apple Account.