Previous Page
Next Page

27.2. AppleScript Studio

AppleScript Studio is a free development environment from Apple allowing you to write Cocoa applications using the AppleScript language. It would require an entire book to discuss AppleScript Studio adequately, so in this section I'll just explain what AppleScript Studio is and how it works, and talk about how you might go about learning it more fully; I'll also provide a simple hands-on example of AppleScript Studio in action.

27.2.1. Cocoa and AppleScript Studio

AppleScript Studio is Cocoa . The precise sense in which I mean this will be clearer in a moment, but it's a simple truth on the face of it, and it means that to understand what AppleScript Studio is, you need to know what Cocoa is.

Cocoa is a massive application framework included as part of Mac OS X. This framework knows how to do all the things that an application might typically wish to do. For example, it can put up windows, in which it can display many different kinds of interface elements for interacting with the user, such as buttons and text fields and sliders and tables and so forth. It also provides very strong text and graphics capabilities. Cocoa is a remarkably well-constructed application framework, striking an excellent balance between power and flexibility; with Cocoa, it's easy to write a simple standard application quite quickly, while at the same time the framework usually provides enough leeway so that the programmer can fully customize the application's behavior if desired. The presence of Cocoa as part of Mac OS X makes it much easier for programmers to write sophisticated, powerful, Mac OS X-native applications, while at the same time such applications often require relatively little code, because so much of the code that does the work resides in the framework.

AppleScript Studio is Apple's way of letting you, the AppleScript programmer, take advantage of the Cocoa application framework without having to learn a different programming language. The "native" Cocoa programming language is Objective-C , and to use Cocoa fully, you would want to learn that language. But as an AppleScript programmer, you might already have written a working script. You don't want to rewrite its functionality in some other language; you want to enhance your script with a more sophisticated user interface than AppleScript alone can provide. AppleScript Studio can let you do this. Think of it as a way to leverage an existing script into a Cocoa application, a way to wrap a Cocoa interface around AppleScript functionality with relative ease.

AppleScript Studio is not, however, a way to take full advantage of the power of Cocoa. By this I mean that you should not expect AppleScript Studio to let you do everything that Objective-C/Cocoa would let you do. If that's what you want, learn Objective-C! AppleScript Studio gives you access to a limited portion of Cocoa's power; that portion is usually enough to let you write a satisfactory application pretty quickly and easily, provided that your needs are fairly simple and your ambitions don't get out of hand. You should not feel disappointed about the fact that AppleScript Studio exposes to the AppleScript programmer only a simplified fraction of Cocoa's abilities. Simple is good. After all, Cocoa is very big, and can require years to learn fully. AppleScript Studio, on the other hand, is relatively tractable.

Let me explain in a bit more technical depth how AppleScript Studio exposes Cocoa to the AppleScript programmer. You'll need a sense of this in order to use AppleScript Studio effectively in any case. Cocoa is an application framework, so it operates through messages that travel back and forth between its code (the code inside the framework) and your code (the code that you actually write, whether you're writing in Objective-C, AppleScript, or whatever). Cocoa is like a gigantic lock, and your code must be structured as a key that fits that lock; you have to write code that will slot into Cocoa's expectations of how an application should work. Thus, even though you can't see inside Cocoa, you do need to know what messages to send to Cocoa and what messages Cocoa will send you, so that its code and your code can work together properly. These messages come under some basic headings:


Built-in methods

The interface elements, such as windows and menus and buttons and text fields, as well as the application as a whole, are prepared to respond to certain messages that you can send. For example, you might like to tell a button to change its title, or ask a text field what the user has typed in it, or tell a window to close, or ask the application whether it is frontmost. You can do these things because the entity to whom you're speaking defines an appropriate message. A button "knows" how to change its title and defines a way for you to tell it to do so; a text field "knows" how to report what the user has typed in it; and so forth. These predefined messages that you can send are the built-in methods .


Action messages

Certain interface elements have a single special behavior when the user performs a certain special operation on them. In the case of a button, that operation is pushing the button. In the case of a text field, it's typing Return within the text field. This is the interface element's action, and when the action occurs, your code can receive an action message so that it can respond.


Lifetime notifications

Cocoa will, if you wish, notify your code of things that routinely take place over the lifetime of an application. For example, you might like your code to be called when your application first starts up, when it comes to the front, or when it is about to quit; or you might like to be informed when the user selects a line in a table or types a character in a text field. These are the lifetime notifications , and again, they exist so that your code has a chance to respond to what's happening.


Delegation queries

Cocoa will sometimes offer your code the opportunity to intervene in its behavior. For example, suppose that the user clicks the mouse to select a line in a table. Normally that line would just be selected, but perhaps your code might have some reason for making it unselectable at that moment. Cocoa can delegate the responsibility for this sort of decision to your code, querying your code as to whether it should proceed normally; if you elect to receive such delegation queries , your code must give a definite answer as to how Cocoa should proceed.

Thus there are two kinds of messages in Cocoathose you send to Cocoa (the built-in methods) and those that Cocoa will send to you (the action messages , lifetime notifications, and delegation queries). Furthermore, your code is essentially idle until something triggers some part of it. An application does nothing unless it is somehow told to do it. And the only way your code can be told to do anything is through a message that Cocoa sends it. In other words, all your code will run only in response to action messages, lifetime notifications, and delegations queries; so your code must be structured, not independently, but as responses to messages from Cocoa.

The architecture I've just described does not perfectly match how AppleScript code works. So to make it match, the Apple folks have interposed a kind of interpreter between Cocoa and your AppleScript code. The interpreter's job is to take care of all the differences between Cocoa and AppleScript, so that you don't have to worry about them. Cocoa sends out a delegation query or a notification message; the interpreter receives this and turns it into AppleScript and routes an appropriate event handler call to your script. Your AppleScript code obtains references to interface elements and targets them with commands, and gets and sets their properties; the interpreter turns this into Objective-C and calls the corresponding built-in method of the relevant Cocoa object. This interpreted linkage between AppleScript and Cocoa is often referred to as a bridge, and we say that the Apple folks have bridged AppleScript to (certain parts of) Cocoa.

The first thing to do in learning AppleScript Studio is to consider learning more about Cocoa. After all, AppleScript Studio is Cocoa, and even if you'd rather not learn any Objective-C, it can be really helpful to have some familiarity with the location of the Cocoa documentation on your hard drive, and perhaps to read a couple of good introductory Cocoa books. (My favorite is Aaron Hillegass, Cocoa Programming for Mac OS X [Addison-Wesley, 2004], 2nd ed.) Furthermore, it's possible that you might end up wanting to learn some Objective-C after all. You can easily find your programming desires thwarted when you encounter an area where AppleScript is not bridged to Cocoa. There are four solutions when this happens:


Give up

If you restrain your desires, your desires can't be thwarted. If something you want to do isn't bridged, stop wanting to do it and confine yourself to what is bridged. This is not an ignoble way out; all programming involves a trade-off of time and effort, and it may be that simplifying your needs is the wisest course.


Use call method

You can call directly from AppleScript into an Objective-C method with the call method command (I'll illustrate its use later on).


Make a hybrid

It's perfectly possible for some of your code to be written in AppleScript, in a script, and for other parts of your code to be written in Objective-C, in a custom class. In effect, such an application is a hybrid of Objective-C/Cocoa and AppleScript Studio.


Give up, the other way

Sometimes AppleScript in Cocoa is a square peg in a round hole. Consider learning Objective-C and writing your application in Objective-C/Cocoa. You can still use AppleScript from within Objective-C (see "Application" in Chapter 2). It may seem paradoxical, but not using AppleScript Studio can sometimes be the wisest development strategy.

27.2.2. The Pieces of AppleScript Studio

AppleScript Studio is like Los Angeles: it isn't actually anywhere. It isn't a thing or a place; it's many tools and resources, used in a certain way. Let's talk about those tools and resources.


The developer tools

The developer tools, collectively known as the Xcode Tools, are on the Tiger DVD, but they are an optional installation. If you don't install them, you won't have any of the AppleScript Studio tools and resources on your hard disk. So install them! An even better approach, as the version on your copy of the Tiger DVD may be outdated, is to obtain the latest version from Apple. First you must join the Apple Developer Connection ; the "online" membership level is free (http://developer.apple.com/membership/online.html). Then you'll be able to log in and download the Xcode Tools .


Interface Builder

Interface Builder, an application located (after you've installed the developer tools) in /Developer/Applications, is where you'll design your application's interface. It's worth perusing the Interface Builder Help early in the game.


Xcode

The Xcode application (not to be confused with the Xcode Tools as a whole) is also in /Developer/Applications. This is where you create and work with a project (the collection of files that will be combined to create your application); it's where you write your code, it's where you access the files that constitute the project, and it's where you'll ask to have your code turned into a real application (called building the project). What makes your project an AppleScript Studio application is that you specify "AppleScript Application" when you create the project.

The fact that you design your interface in one application but edit your code and build the application in another is a tricky aspect of the Cocoa development experience, and takes some getting used to. As a beginner, you should work on only one project at a time; while doing so, keep both Interface Builder and Xcode running but hide whichever you're not using at that moment.


Tutorial

There is a hands-on tutorial at /Developer/ADC Reference Library/documentation/AppleScript/Conceptual/StudioBuildingApps/. I have reservations about its appropriateness (it employs some techniques I regard as inadvisable), but it's probably worth going through it, if only as a way of becoming familiar with Xcode and Interface Builder.


Reference

The most important AppleScript Studio documentation is the reference document at /Developer/ADC Reference Library/documentation/AppleScript/Reference/StudioReference/. You should skim through this at the outset and then expect to refer to it constantly while working.


The dictionary

The terminology for the repertory of things you can say in an AppleScript Studio application is defined in a dictionary. (You don't have to target any application in your AppleScript Studio code in order to gain access to this terminology; it is made automatically available.) This dictionary appears in your project as AppleScriptKit.sdef; double-click it to view it in a dictionary display. This is essentially the same information as in the reference, but not as well presented, so you're less likely to use it.


Examples

There are many AppleScript Studio examples located in /Developer/Examples/AppleScript Studio. They explore and demonstrate most of the important aspects of using AppleScript Studio to drive the user interface; it's very worthwhile to study them.


Cocoa documentation

There are good links to help you find your way into Cocoa, Objective-C, and Cocoa documentation in an introductory document at /Developer/ADC Reference Library/referencelibrary/GettingStarted/GS_Cocoa/. Furthermore, each page about an AppleScript class in the AppleScript Studio reference document is cross-linked to the page about the corresponding Cocoa class; the discussion of the Cocoa class is well worth consulting, as it often explains things better, and frequently has links to even more useful pages on general topics about how things work in Cocoa.

27.2.3. AppleScript Studio Example

As a tutorial example, let's return to the code presented earlier for searching the TidBITS online archive (in Chapter 25). Recall what it does. We allow the user to enter search terms. We use curl to submit those terms to the TidBITS search engine. The reply is a page of HTML listing the pages found. We use Perl to parse the HTML, and present the results to the user as a list. If the user double-clicks a listed article, we present the corresponding page in the web browser. The purpose of the tutorial is to illustrate AppleScript Studio development by wrapping this script in a nice interface. We'll have two windows, a Search window where the user enters search terms and a Results window where the results will be listed.

Begin by creating the project. Start up Xcode. Choose File New Project and select "AppleScript Application"; in the dialog that appears, name the project SearchTidBITS and finish creating it.

We must embed the Perl script into the bundle of the built application. This must be done by the build process, so the Perl script must be incorporated into the project. Select the Resources folder on the left side of the project window and choose Project Add Files. In the Open File dialog, find and select the Perl script, which is called parseHTML.pl. In the next dialog, check the box at the top which asks whether you want to copy the file into the project, and click the Add button.

Now we'll design the interface. In the project window, double-click MainMenu.nib. Interface Builder will open. I'm going to skip some details ("drag this interface element from this palette onto this window, resize it, make these settings in the Attributes pane of the Inspector," and so forth) and simply show you the interface design we're going to construct. It consists of two windows and some changes to the menu bar:

  • The Search window contains an NSForm displaying three fields the user can search on (text, title, and author) and a Search button, along with a spinning progress indicator to provide feedback while we're talking to the Internet (Figure 27-1).

    Figure 27-1. Search window

  • The Results window contains a single-column table for displaying the titles of the found articles, along with some explanatory text telling the user what to do (Figure 27-2).

  • In the menu bar, I remove the File menu and replace it with a Search menu consisting of a single New menu item; also, I add a Close menu item to the Window menu. Finally, I run through the menu items of the application menu and the Help menu and replace the placeholder "NewApplication" with "SearchTidBITS," the name of our application (Figure 27-3).

Now comes the really interesting part. As you'll recall, the Cocoa framework defines certain action messages, lifetime notifications, and delegation queries that can be sent to your code. In Interface Builder, we must specify which of these messages we want to receive, with respect to each element of our interface as well as the application as a whole. (Remember, we must elect to receive some of these messages or our code will never run.) At the same time, we should also give at least some of our interface elements AppleScript names, so that our AppleScript code can refer to them. All of this is done in the AppleScript pane of the Inspector window.

Figure 27-2. Results window


Figure 27-3. The menu bar


Figure 27-4 shows the process. I've selected the Search window (not shown) and now I focus my attention on the Inspector and its AppleScript pane. In the Name field I've entered an AppleScript name, "search"; this will allow our code to refer to this window by name, as window "search". In the list of event handlers , I've checked the "will open" checkbox; this means that our code will receive a will open event at the appropriate moment in the lifetime of this window (namely, when it is about to open). Somewhat confusingly, it is not sufficient to check this checkbox; it is also necessary to check the Script checkbox specifying the file SearchTidBITS.applescript, so that AppleScript Studio knows what script to send the will open event to (even though our project has in fact only one script).

Figure 27-4. AppleScript Inspector for the Search window


Here are the interface items of our project and the treatment I apply to each:


File's Owner

This icon in the main Interface Builder window represents the application as a whole. I want to know when the application has launched so that I can perform some initializations, so I check Application: launched (a lifetime notification).


Search New menu item

I want to know when the user chooses this menu item, so I check Menu: choose menu item (the menu item's action message).


Window Close menu item

I want the frontmost window to close when the user chooses this menu item. I could check Menu: choose menu item for this menu item as well, but the knowledge of how to close a window is already built into Cocoa, so there is no need to involve AppleScript or any code at all. Instead, I form a Cocoa connection. To do so, I Control-drag from this menu item to the First Responder icon in the main Interface Builder window, and connect it to the First Responder's performClose: method.


Search window

I name this window search. I want to know when this window is about to open, because I want to make some interface adjustments; so I select Window: will open (a lifetime notification).


Search window: the Search button

I want to know when the user clicks this button, so I select Action: clicked (the button's action message).


Results window

I name this window results. I want to know when the user tries to close this window, so I check Window: should close (a delegation query).


Results window: the table view

I want to know when the user double-clicks a row of the table view, so I select Action: double clicked.

This completes our use of Interface Builder, so we save our work and switch to Xcode. Find the SearchTidBITS.applescript file in the Xcode project window and open it. Templates for the event handlers that we specified in Interface Builder have already been created:

on will open theObject
    (*Add your script here.*)
end will open
 
on launched theObject
    (*Add your script here.*)
end launched
 
on choose menu item theObject
    (*Add your script here.*)
end choose menu item
 
on clicked theObject
    (*Add your script here.*)
end clicked
 
on double clicked theObject
    (*Add your script here.*)
end double clicked
 
on should close theObject
    (*Add your script here.*)
end should close

Now we'll write our script's code, filling in these event handlers and adding any further user handlers of our own. Let's tour the final code a little at a time. In Example 27-1 we declare some top-level globals. We also write code for the launched handler that will be called right after the application has started up; this is the best place for general initializations, which is just what we use it for here, locating the Perl file within our application's bundle and retaining its POSIX pathname as a global.

Example 27-1. Globals and launched handler
global perlScriptPath, L1, L2, textSought, titleSought, authorSought
 
on launched theObject
    local f
    set f to (path to resource "parseHTML.pl")
    set perlScriptPath to quoted form of (POSIX path of f)
end launched

Example 27-2 shows the will open handler. Observe that these event handlers have a parameter, theObject; this contains a reference to the interface element with which this action message, notification, or delegation query is associated. In this case, we know that only the Search window is set to deliver a will open notification, so there is no need to bother checking theObject's identity; we just blithely assume that it's the Search window.

Example 27-2. The will open handler
on will open theObject
    tell theObject
        call method "setDisplayedWhenStopped:" of progress indicator 1 with parameter 0
    end tell
end will open

The code illustrates the use of call method to form a manual bridge from AppleScript to Objective-C. We want to make sure that the progress indicator is invisible when not spinning. This setting is available through a built-in Cocoa method, but no AppleScript property is bridged to it, so we cross the bridge ourselves; in effect, we form the equivalent of the following line of Objective-C code:

[theProgressIndicator setDisplayedWhenStopped: NO];

Example 27-3 shows the choose menu item event handler, which is the action message from the Search New menu item. We hide the Results window and show the Search window, emptying the form cells and selecting the first cell, ready for the user to enter text. Once again, AppleScript is not bridged to a built-in Cocoa method that we want to call, so we use the call method command to call it directly.

Example 27-3. The choose menu item handler
on choose menu item theObject
    hide window "results"
    tell window "search"
        tell matrix 1
            set content of cell 1 to ""
            set content of cell 2 to ""
            set content of cell 3 to ""
            call method "selectTextAtIndex:" of it with parameter 0
        end tell
        show
    end tell
end choose menu item

Example 27-4 shows the clicked handler. This is the Search button's action message, and is the heart of our application. To make the code clearer, I've broken the functionality out into some ancillary user handlers. We start by initializing our globals based on what's in the Search window form; then, after a sanity check, we call the next user handler, doTheSearch.

Example 27-4. The clicked handler and an associated utility
on clicked theObject
    tell matrix 1 of window "search"
        set textSought to my urlEncode(content of cell 1)
        set titleSought to my urlEncode(content of cell 2)
        set authorSought to my urlEncode(content of cell 3)
    end tell
    if length of textSought < 5 and length of titleSought < 5 and 
length of authorSought < 5 then beep return end if doTheSearch( ) end clicked   on urlEncode(what) set text item delimiters to "+" return (words of what) as string end urlEncode

Example 27-5 is a utility hander, feedbackBusy, purely for manipulating the interface to provide some user feedback. We're going to be talking to the Internet by way of curl, and while we're doing this, nothing will be happening. The user might think that the application is idle or broken. Therefore we spin the progress indicator and disable the Search button to give the user a sense that the application is busy and that he should keep his hands off while it does whatever it's doing. The handler is called with a boolean parameter telling whether to begin or end this feedback. Once again there's a use of call method to make up for a deficiency in the bridging: here, we force the window to update its display so that the disabled or enabled Search button will look disabled or enabled.

Example 27-5. The feedbackBusy handler
on feedbackBusy(yn)
    tell window "search"
        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
        call method "display" of it
    end tell
end feedbackBusy

Example 27-6 shows the doTheSearch handler. This should seem familiar, being nearly unchanged from the code in Chapter 25. The main differences are:

  • The post argument now incorporates values from the three different form fields the user is allowed to fill out.

  • We ask for 2,000 articles instead of 20. The reason is that we're hoping to capture the titles of all the found articles. The search was originally constructed so that its results could be displayed in a web page, where you're supposed to find the first 20 results, then ask for another page showing the next 20, and so forth. I originally thought of trying to emulate this in our application. But then it struck me that we've got this nice scrolling table view to play with, and displaying a large number of titles is no problem, so we may as well gather lots of them in one search and be done with it.

  • Interface feedback is provided to the user through calls to feedbackBusy before and after the call to curl.

  • The call to curl now has a few more parametersfor example, we provide some timeout values, because the TidBITS search server can be rather slow.

  • The intermediary file is now located in the temporary items directory, where the user won't see it (it will be deleted automatically when the user logs out).

  • Error handling has been added. It's primitiveif there's a problem, we beepbut this is enough to prevent any mysterious error messages from appearing before the user's eyes. The problem will usually be either that no results were obtained from the search or that the search was never run because we couldn't connect to the server; in real life it might be nice to distinguish these cases and to provide nice error messages, but this is left as an exercise to the reader (meaning that I was too lazy to do it myself).

Example 27-6. The doTheSearch handler
on doTheSearch( )
    local d, f, r
    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 (POSIX path of (path to temporary items)) & "tempTidBITS"
    feedbackBusy(true)
    try
        do shell script 
"curl -s --connect-timeout 25 -m 120 -d " & d & " -o " & f & " " & u set r to do shell script ("perl " & perlScriptPath & " " & f) feedbackBusy(false) set L to paragraphs of r set half to (count L) / 2 set L1 to items 1 thru half of L set L2 to items (half + 1) thru -1 of L displayResults( ) on error feedbackBusy(false) beep end try end doTheSearch

If the doTheSearch handler doesn't error out, it calls displayResults to populate the Results window and present it to the user. Example 27-7 shows the displayResults handler, along with two event handlers connected with the Results window. The double clicked event handler responds when the user double-clicks a line of the table of article titles: the corresponding URL is sent to the web browser for display. The should close handler works around a bug in Tiger's version of AppleScript Studio (at least I think it's a bug): a window that might be shown again later must not be closed, as if it is shown again later it will malfunction. Therefore when the user attempts to close the Results window we prevent it (by returning false) and hide the window instead; it looks to the user as if the window has closed, but it hasn't.

Example 27-7. The displayResults handler
on displayResults( )
    tell table view 1 of scroll view 1 of window "results"
        set contents to L2
    end tell
    show window "results"
end displayResults
 
on double clicked theObject
    try
        open location (item (clicked row of theObject) of L1)
    end try
end double clicked
 
on should close theObject
    hide theObject
    return false
end should close

This completes the development of our application. To test it, choose Build Build and Run in Xcode. (Figure 27-5 shows the running application in action; we've performed a search for articles by our favorite author mentioning our favorite subject, and the first article found is being displayed in a web browser in the background.) To prepare your application for public release, choose Project Set Active Build Configuration Release; then choose Build Clean All Targets and then Build Build. The result is a more compact application that will run on other users' machines, with the script saved as run-only to hide it from prying eyes.

Figure 27-5. SearchTidBITS in action


27.2.4. Automator Actions

An Automator action (see "Automator" in Chapter 2) is an excellent way to wrap some AppleScript code with a lightweight interface. An Automator action is not a standalone application; rather, it is hosted by Automator (or by some other environment that can run Automator workflows). An action typically has no windows or menus; rather, a single pane (technically, an NSView) appears as the action's interface within Automator, and optionally can appear when the workflow runs, as a way of supplying the script with parameters. This isn't much interface, but in many cases it will be just enough. The script also receives as a parameter the output values from the previous action in the workflow. An Automator action thus gives the end user more power and flexibility than a pure script: the end user can position the action within a larger workflow, and can set options in its interface, effectively repurposing the script and customizing its behavior without seeing or editing its code (and without having to know any AppleScript). To write an Automator action is not difficult, and takes only a few minutes; and it can be done using AppleScript Studio.

Here's a hands-on tutorial illustrating the process of writing an Automator action. (See /Developer/ADC Reference Library/documentation/AppleApplications/Conceptual/AutomatorConcepts for Apple's full documentation.) For our example, we'll write an action that accepts file aliases and encodes those files as MP3s using lame (http://lame.sourceforge.net). It is assumed that lame is installed in its default location, which is /usr/local/bin. In our action's interface, we'll provide an option for selecting a preset and a bitrate. MP3 encoding is a time-consuming activity, so our implementation will script the Terminal (rather than calling do shell script) so the user can see some feedback as the encoding proceeds.

Start up Xcode. Choose File New Project and select AppleScript Automator Action; name the project LAME Encode. When the project window appears, choose File New File to create an AppleScript Text File called ui.applescript to be added to the project. The reason is that we wish to write some code that will interact with the action's interface, and this code must not go into main.applescript, the only script supplied by default.

Now let's create our action's interface. In the project window, double-click main.nib to start up Interface Builder. The View window displays the pane in which the Action's interface is to appear. Start by removing the placeholder text ("UI elements go here"). Apple's interface guidelines for Automator actions specify that interface elements should be Small size rather than Regular, and that the NSView should have 10-pixel margins. They also suggest using space economically, and in particular they ask that a popup menu should be preferred to radio buttons.

Figure 27-6 shows our action's interface. The top row contains the interface elements the user can set. The popup menu contains the names of the most important presets: its items are "insane," "extreme," "standard," "medium," a menu item separator, then "studio," "cd," "hifi," "tape," and "mw-us." The second row contains a text field that we'll use to show the user the results of the current settings in the top row. We won't implement VBR bitrates, so the bitrate text field will be disabled unless the "cbr" checkbox is checked. The height of the NSView has been reduced as much as possible; vertical space is at a premium when the user is constructing a workflow in Automator, so we don't want to waste any.

Figure 27-6. The action's interface


Just as in the AppleScript Studio tutorial, earlier in this chapter, we now give our interface elements AppleScript names and arrange for notification when the user changes one of them. The names are namePopup, fastSwitch, cbrSwitch, cbrText, and example. The notifications must all be sent to ui.applescript; they are the popup menu's action, the clicked of the two checkboxes, and the bitrate text field's action and changed, along with the NSView's update parameters and awake from nib.

We can use Cocoa bindings to tie our interface items to parameter values that our main.applescript code will receive. That's why the NSObjectController called "Parameters" is present in the main window. The use of bindings isn't compulsory, but in this case we can save ourselves a bit of coding by binding the "cbr" checkbox's value to a key "cbrSwitch" and the bitrate text field's value to a key "cbrText." We can now immediately bind the bitrate text field's enabled to the "cbr" checkbox; thus, the text field will be enabled or disabled automatically as the user checks or unchecks the check box. Also, later on we'll be able to supply a default value for the bitrate text field without writing any code. (Figure 27-7 shows the bindings for the bitrate text field as displayed in the inspector after creating them.)

This completes the design of the interface, so save and quit Interface Builder. Back in Xcode, now comes the touchiest part of the processediting the Info.plist file. Starting with Xcode 2.1 this task is has been made somewhat easier, because it can be

Figure 27-7. Binding an interface element


done mostly in the target's info window. Choose Project Edit Active Project to bring up the info window, and switch to the Properties pane; in the lower half is the Collection popup menu, and the idea is to choose each of its items in turn and edit, as necessary, the members of that collection. Here are the settings I've elected to use:

Collection

Setting

Value

General

Action name

LAME Encode

General

Application

iTunes

General

Category

iTunes

General

Icon name

iTunes

Parameters

cbrSwitch

Type boolean; value 0

Parameters

cbrText

Type string; value 128

Parameters

exampleText

Type string; value "dummy"

Description

Summary

Encodes files to MP3.


Also, in the General collection, check "Can show when run" at the bottom of the window.

Observe the use of the Parameters settings to supply part of our interface with initial values by way of the bound keys; so, for example, the bitrate field will initially contain "128" because we say so here. It is crucial that the name and spelling for the keys, such as cbrText, should match exactly between Interface Builder (where the interface item is bound to that key) and here (where the key's initial value is given).

You should be wondering what the exampleText parameter is for, as we have no interface element bound to this key. This parameter is part of a trick that will be used later to communicate between our ui.applescript and main.applescript files, so read on and all will be made clear.

So much for the info window, but unfortunately we still need to edit Info.plist a bit further by hand. Select Info.plist in the project window and choose File Open With Finder to bring up the file for editing in Property List Editor. (Under no circumstances should you attempt to edit this file by hand as a text file, as you will invariably make a mistake and render the file invalid as a property list.) Open each triangle and select and delete each line that we've left as a default comment (such as AMDInput and most of the other entries under AMDescription). When you're done with all that, save the file and quit Property List Editor. But, alas, we are still not done. We must also edit the localised versions of these settings, in the file called InfoPlist.strings. Open this file as a text file and delete everything except the CFBundleName and the AMName, and save it.

We are now ready, at long last, to write our code. Start with ui.applescript. Remember, the purpose of this script is to interact with our action's interface. The code is almost identical to what we'd put in a standalone application built with AppleScript Studio. I'll present the code a bit at a time.

Example 27-8 shows start of the code. The very first thing is to capture a reference to the NSView that contains the interface elements so that the rest of our code can refer to them. (Unlike our earlier standalone application example, we have no globally available named window, such as window "search", on which to base a reference to an interface element.) We have set the NSView to send us an awake from nib notification; this notification is guaranteed to arrive very early when our action's interface loads, and since theObject in this case in the NSView itself, we simply copy it to a property. To this and all the other action messages and notifications, we respond by calling our updateInterface handler; thus our interface will be "live," updating itself whenever the user does anything.

Example 27-8. Keeping the interface updated
property theView : missing value
 
on awake from nib theObject
    set theView to theObject
    updateInterface( )
end awake from nib
 
on action theObject
    updateInterface( )
end action
 
on clicked theObject
    updateInterface( )
end clicked
 
on changed theObject
    updateInterface( )
end changed

Example 27-9 shows the updateInterface handler. The idea is to construct the lame command based on what the user has selected and typed in the interface, displaying this command in the example text field in the action's interface. To keep things simple, I have omitted to perform certain validations; in particular, the user can enter anything in the bitrate text field. (If the value is an unreasonable number, we use it anyway, and if it's not a number, we treat it as 128.)

Example 27-9. Constructing the lame command
on updateInterface( )
    tell theView
        set s to title of popup button 1
        set s1 to "lame --preset "
        if (state of button "fastSwitch") is 1 then
            if state of button "cbrSwitch" is 0 then
                if s is in {"medium", "standard", "extreme"} then
                    set s1 to s1 & "fast "
                end if
            end if
        end if
        if (state of button "cbrSwitch") is 1 then
            try
                set s to "cbr " & (content of text field "cbrText" as integer)
            on error
                set s to "cbr 128"
            end try
        end if
        set content of text field "example" to s1 & s
    end tell
end updateInterface

Now comes the clever part (Example 27-10). We want main.applescript to receive, among its parameters, the updated value of the example text field, as this is the lame command to be sent along to the Terminal. These parameters are supplied as a record whose items are based on the parameters listed in Info.plist. That's why we created the exampleText parameter in Info.plistto make it appear in the parameters record. In addition, we have implemented the update parameters event handler, whose purpose is exactly to give us a chance to modify the values in the parameters record. This event handler will be called just before the workflow runs; the record containing the parameters arrives under the name theParameters. So we update the interface one last time, and then we modify the exampleText item of theParameters and hand back the modified record.

Example 27-10. Passing a parameter to main.applescript
on update parameters theObject parameters theParameters
    updateInterface( )
    set |exampleText| of theParameters to (content of text field "example" of theView)
    return theParameters
end update parameters

Now we are ready for main.applescript (Example 27-11). This script contains just one event handler, the run handler , which has been created for us. It takes two parameters: input, which is the output from the previous step in the workflow, and parameters, which contains the bound values from the interface along with the modifications made in our update parameters handler. We assume that input is a list of aliases; we convert these aliases to POSIX pathnames. We extract the exampleText item from the parameters record; this is the lame command the user wants us to perform. Now we form a shell command that will loop through each POSIX pathname in turn and hand it to our lame command. (The shell is assumed to be bash.) We send this shell command to the Terminal for execution. The Terminal will execute this command asynchronously (we have specified that are ignoring application responses), so our handler will end immediately; we must return something to serve as our action's output, even though the action produces no meaningful output, so on the principle of doing least harm we return the same list we received as input.

Example 27-11. The action's main script
on run {input, parameters}
    set myInput to (input as list)
    set L to {}
    repeat with anAlias in input
        set end of L to quoted form of POSIX path of anAlias
    end repeat
    set text item delimiters to space
    set theFiles to L as string
    set s to "arr=(" & theFiles & "); "
    set s to s & "for i in \"${arr[@]}\"; "
    set s to s & "do echo /usr/local/bin/" & |exampleText| of parameters & space
    set s to s & "\"$i\" \"${i%\\.*}.mp3\"; done"
    ignoring application responses
        tell application "Terminal" to do script s
    end ignoring
    return input
end run

The really cool part is that, because we have allowed it in Info.plist, the user can now choose "Show Action When Run" to display our action's interface in a workflow at runtime (Figure 27-8).

Figure 27-8. Choosing to show the action's interface at runtime


This gives the user maximum flexibility. Let's say, for example, that the user creates a workflow consisting of just our LAME Encode action, with "Show Action When Run" checked, and saves it as a Finder plug-in. This means that in the Finder this workflow will appear as a contextual menu item. The user can select some sound files in the Finder and, using the contextual menu, run this workflow. Our action's interface will appear as a dialog in the Finder! (See Figure 27-9.) The user can then specify the desired settings and continue with the workflow; the files will be converted to MP3 format, with feedback in the Terminal. This illustrates my point about an Automator action having just enough interface.

Figure 27-9. Our Automator action at work



Previous Page
Next Page