MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Building Applications with AppleScript and FaceSpan

by Matt Neuburg, author of AppleScript: The Definitive Guide
04/13/2004

AppleScript is primarily a scripting language; it is intended to let the user communicate with existing applications. Still, having developed a scripting solution with AppleScript, a user might naturally wish to wrap a standard application interface around it. So, how can a user take advantage of AppleScript in order to write a stand-alone application?

In my book, AppleScript: The Definitive Guide, attention is given (at the end of Chapter 2, and as the main subject of Chapter 24) to this very question. But at the time of the book's publication, FaceSpan for Mac OS X did not yet exist, so it was not considered in the repertory of tools one can use for building an application with AppleScript. Now FaceSpan 4.0 has emerged, so in this article, I'll briefly describe how FaceSpan may be used to construct the example applications in chapters 2 and 24 of my book.

What Is FaceSpan?

FaceSpan 4.0 is a small (7MB) Cocoa application in which you build a Cocoa application, using AppleScript as a programming language. You "draw" your interface in a window editor, adding interface elements to a window and specifying some of their physical behavior in an Info Panel (the palette labeled "window" in the lower right of the following figure). Then you write AppleScript code saying what should happen when the user interacts with those interface elements to generate an event (starting up the application, choosing a menu item, pressing a button, and so forth).

Related Reading

AppleScript: The Definitive Guide
By Matt Neuburg

FaceSpan uses the same underlying dictionary as AppleScript Studio for referring to and communicating with the parts of the interface (the AppleScriptKit dictionary). Indeed, working in FaceSpan is much like working in AppleScript Studio, except that you are spared having to operate in two different applications: in AppleScript Studio, you "draw" your application's interface in Interface Builder and you write and test your code in Xcode, but FaceSpan is completely self-contained. Some further differences between the two environments will be noted as we go along.

The biggest difference, of course, is that AppleScript Studio is free, whereas FaceSpan costs $200 (or $90 for a limited version that builds applications that require the presence of FaceSpan itself in order to run). The question of whether FaceSpan possesses a sufficient superiority over AppleScript Studio to justify this pricing is a matter for the free market to decide; in other words, only time will tell.

The Disk Lister Example

Let's start with the "disk lister" example on pp. 31-36 of my book. This, you recall, is intended simply to illustrate AppleScript driving an external application (the Finder) from within a stand-alone application. Our application merely displays the names of all mounted drives (or partitions) in a table view in a window. My book shows how to do this using AppleScript Studio, REALbasic, and Cocoa/Objective-C; now we'll add FaceSpan to our box of tools.

Start up FaceSpan and ask for a New Project; call it "Disk Lister," and use the default template. In a moment, the Project Window appears, as shown in the following figure:

Double-click the "main" window listing to open it so we can design our window. Show the Objects Palette if it isn't showing already, open the Tables section, and drag a table view into the main window (see the figure below).

Show the Info Panel if it isn't showing already, and double-click the table view in the "main" window so that the Info Panel is talking about the table view, not the scroll view that contains it. Set the number of columns to 1, and select the column header so that the Info Panel is now talking about the table column. Give that column a title "Your Disks" and an identifier "disks" (see figure below).

Now, adjust the size of the window and the table view as you like them. So much for designing the interface.

Now we'll write the code. In the Project Window, open the script provided for you, Project Script.applescript. You'll find that there is already a launched handler:

on launched theApplication
    open window "main"
end launched

Modify this by adding the very same code that appears on page 32 of my book:

on launched theApplication
    open window "main"
    tell application "Finder" to set L to (name of every disk)
    set ds to make new data source at end of data sources
    set tv to table view 1 of scroll view 1 of window 1
    set col to make new data column at end of data columns of ds ¬
        with properties {name:"disks"}
    repeat with aName in L
        set aRow to make new data row at end of data rows of ds
        set contents of data cell "disks" of aRow to aName
    end repeat
    set data source of tv to ds
end launched

That's all. Press Command-R to save the project and run the built application. After a moment's delay, the Disk Lister application appears and displays its window (see figure below).

A nice feature of FaceSpan is the Object Browser (see figure below) that appears in the Scripting Help drawer attached to script windows. It lists objects in the application's interface, and you can double-click a listing to insert a reference to that object in your code. For example, if we didn't know that we can access our table view as "table view 1 of scroll view 1 of window 1," the Object Browser would tell us how to do it (using names instead of index numbers).

If you were writing this application and you didn't know how to use table views and data sources, you'd read the FaceSpan manual; you would also consult the dictionary, which is displayed in a window (shown below) that looks a little different from how the Script Editor or Xcode presents it.

The Search TidBITS Example (Interface)

The Search TidBITS example in Chapter 24 of my book is rather more elaborate, involving interactive interface elements, multiple windows, and an embedded Perl script. The purpose of the application is to act as a front end to the TidBITS archive server. The user enters text in some search fields, and presses a button; the application constructs an HTTP POST request and uses curl to submit it to the TidBITS server. When the response comes back as an HTML page, the application uses Perl to parse the HTML, extracting the titles and URLs of the found articles, and presents the list of titles to the user; the user can then double-click on a title to open the corresponding URL in a browser.

To create the Search TidBITS example, start up FaceSpan and create a new project called "Search TidBITS". In the Project Window, select Other and use the contextual menu's Add Files item to copy the parseHTML.pl Perl script into the project; this insures that the Perl script will appear in the built application's package, within its Contents/Resources directory, where we will be able to access it from our running code. Here is the Perl script (p. 364):

$s = "";
while (<>) {
    $s .= $_;
}
$s =~ m{search results (.*)$}si;
$1 =~ m{<table(.*?)</table>}si;
@rows = ($1 =~ m{<tr(.*?)</tr>}sig);
for ($i=0;$i<$#rows;$i++) {
    ($links[$i], $titles[$i]) = 
        ($rows[$i+1] =~ m{<a href="(.*?)">(.*?)</a>}i);
}
print join "\n", @links, @titles;

In the Project Window, select Windows and use the the contextual menu's New Window item to create the second window. Now you can design the interface. First, design the windows; this is like the design in the AppleScript Studio implementation, except that FaceSpan has no NSForm control, so I just use separate text fields to make up the Search window (see figure below).

Now, we'll design the menu. We provide two menu items of our own, New and Close, as shown in the following figure:

If you want a menu item to be active and to send an event when it is chosen, you must select it and choose the "execute script" action in the Info Panel. So, this must be done now for the New and Close menu items.

Next we assign names to those interface elements that need them, by selecting each element and typing into the Info Panel: I've called the two windows "main" and "results"; in the "main" window, the three editable text fields have been named "textField," "titleField," and "authorField". Other interface elements can be identified easily enough by index number or in some other way, so there's no need to assign any further names.

The Search TidBITS Example Code

Next, we'll write our application's code. There are two big differences between what you do in AppleScript Studio and what you do in FaceSpan.

First, in AppleScript Studio, you must use the Info Window to declare explicitly what events sent by each interface item you wish to handle. In FaceSpan, there is no need for this; if you want to handle an event, you just write a handler for it.

Second, FaceSpan permits you to organize your code according to a container script inheritance hierarchy. The idea is that different interface elements can have scripts of their own. Behind the scenes, these scripts are related using script inheritance (see p. 178 of my book). Therefore, a script can "see" handlers and globals in the script of the interface element's container; and when an interface element generates a user event, the event propagates up the container hierarchy, starting with the script of the interface element itself, until it finds a matching handler. This permits an object-based style of code organization. This code organization, while purely optional, can be considerably neater than what you have to do in AppleScript Studio.

To illustrate, we're going to make three more scripts, each attached to a particular interface element: one for the push button within the "main" window; one for the "main" window itself; and one for the table view within the "results" window. Thus, counting the Project Script.applescript script that comes with the project, we will end up with four scripts.

These scripts will automatically form a hierarchy corresponding to the container hierarchy. (I have put the the "results" window script in brackets, because we're not going to bother creating it.)

Project Script.applescript
    "main" window script
        push button script
    ["results" window script]
        table view script

A script at a lower level of the hierarchy can call a handler or (using the keyword my) access a property in a script at a higher level above it. Furthermore, propagation of a user event will start with the script of the interface element that generated the event.

For example, both a push button and a table view will generate a "clicked" event when the user clicks on them. In AppleScript Studio, you might have just one "on clicked" handler where both these events would arrive; that handler would then have to distinguish which interface element generated the event, in order to decide what to do. But in FaceSpan there is no need for this. We can put an "on clicked" handler in the push button script, and it will be called only when this push button is clicked.

To create a script associated with an interface element, select it in its window editor and use the contextual menu to choose Edit Script. After you've created a script for an interface element, FaceSpan displays a little script icon in the lower left of that element.

Having created our three additional scripts, we can organize our code. There are many ways to do this, so I'll just make some arbitrary decisions. The top-level Project Script.applescript holds just those properties and handlers that reasonably should go at the top level of our code structure:

property perlScriptPath : ""
property L1 : {}
property L2 : {}

on launched theApplication
    local f
    open window "main"
    set f to resource path of main bundle
    set perlScriptPath to POSIX path of ¬
        POSIX file (f & "/parseHTML.pl")
    set perlScriptPath to quoted form of perlScriptPath
end launched

on choose menu item theMenuItem
    local theTitle
    set theTitle to (get title of theMenuItem)
    if theTitle is "New" then
        show window "main"
        hide window "results"
        tell window "main"
            set string value of text field "textField" to ""
            set string value of text field "titleField" to ""
            set string value of text field "authorField" to ""
        end tell
    else if theTitle is "Close" then
        hide window 1
    end if
end choose menu item

on displayResults()
    local ds, tv, col, aName, aRow
    set ds to make new data source at end of data sources
    set tv to table view 1 of scroll view 1 of window "results"
    set col to make new data column at end of data columns of ds ¬
        with properties {name:"titles"}
    repeat with aName in L2
        set aRow to make new data row at end of data rows of ds
        set contents of data cell "titles" of aRow to aName
    end repeat
    set data source of tv to ds
    show window "results"
end displayResults

(Unfortunately, the "on choose menu item" handler is called when the user chooses either of our two menu items, just as in AppleScript Studio; FaceSpan provides no way to give a menu item its own script.)

The push button script simply receives the "clicked" event and calls a handler in the "main" window script:

on clicked theObject
    startNewSearch()
end clicked

The "main" window script does all the work after the user clicks the push button:

property textSought : ""
property titleSought : ""
property authorSought : ""

on startNewSearch()
    tell window "main"
        set textSought to 
            (get string value of text field "textField")
        set titleSought to 
            (get string value of text field "titleField")
        set authorSought to 
            (get string value of text field "authorField")
    end tell
    urlEncodeStuff()
    doTheSearch()
end startNewSearch

on urlEncodeStuff()
    set textSought to urlEncode(textSought)
    set titleSought to urlEncode(titleSought)
    set authorSought to urlEncode(authorSought)
end urlEncodeStuff

on urlEncode(what)
    set AppleScript's text item delimiters to "+"
    return (words of what) as string
end urlEncode

on feedbackBusy(yn)
    tell window "main"
        if yn then
            set enabled of button 1 to false
            start progress indicator 1
        else
            set enabled of button 1 to true
            stop progress indicator 1
        end if
    end tell
end feedbackBusy

on doTheSearch()
    local d, u, f, r, L, half
    set d to "'-response=TBSearch.lasso&-token.srch=TBAdv"
    set d to d & "&Article+HTML=" & textSought
    set d to d & "&Article+Author=" & authorSought
    set d to d & "&Article+Title=" & titleSought
    set d to d & "&-operator"
    set d to d & "=eq&RawIssueNum=&-operator=equals&ArticleDate"
    set d to d & "=&-sortField=ArticleDate&-sortOrder=descending"
    set d to d & "&-maxRecords=2000&-nothing=MSExplorerHack&-nothing"
    set d to d & "=Start+Search' "
    set u to "http://db.tidbits.com/TBSrchAdv.lasso"
    set f to "/tmp/tempTidBITS"
    feedbackBusy(true)
    try
        do shell script ¬
            "curl -s --connect-timeout 15 -m 120 -d " ¬
            & d & " -o " & f & " " & u
        set r to do shell script ¬
            ("perl " & my perlScriptPath & " " & f)
        feedbackBusy(false)
        set L to paragraphs of r
        set half to (count L) / 2
        set my L1 to items 1 thru half of L
        set my L2 to items (half + 1) thru -1 of L
        displayResults()
    on error
        feedbackBusy(false)
        beep
    end try
end doTheSearch

And here is the code for the table view script:

on double clicked theObject
    set r to clicked row of theObject
    if r is less than or equal to (count my L1) then
        open location (item r of my L1)
    end if
end double clicked

The code itself is just like the AppleScript Studio code in my book. The difference here is simply in its organization. And note that this is not the only way to organize the code! For example, in the "main" window script I call the "displayResults" handler. That handler, for purposes of this example, is located in the top-level Project Script.applescript script. But it doesn't have to be there. It could just as well be in "main" window script, or even in the push button script! The point is that FaceSpan can help you use whatever style of organization feels natural to you as you develop and maintain your code.

Final Thoughts

In its earlier incarnations, FaceSpan had an enthusiastic base of users, who have naturally been clamoring for a Mac OS X update. Now such an update exists, and it promises to be an easy and enjoyable way to develop stand-alone applications with AppleScript. It will be interesting to see how further development of FaceSpan pans out. If FaceSpan 4.0 had existed when I was writing my book, I would certainly have included some discussion of it, so now that it does exist, I'm glad to have been able to post this little online addendum to my book.

Matt Neuburg is the author of O'Reilly's "AppleScript: The Definitive Guide," "REALbasic: The Definitive Guide," and "Frontier: The Definitive Guide," and is a former editor of MacTech magazine.


Return to the Mac DevCenter

Copyright © 2009 O'Reilly Media, Inc.