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.
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 |
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.
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 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.
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.
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.