27.1. AppletsAn applet is a compiled script wrapped up in a simple standalone application shell (see "Applet and Droplet" in Chapter 3). To make a script into an applet, save it from a script editor application as an application instead of as a compiled script. You select this option in the Save As dialog. When you launch the resulting application (by double-clicking it in the Finder, for example), the script runs. It is also possible to save a script as an application bundle. From the outside, this looks and works like an applet. Because it's a bundle, though, you can do things with it that you can't do with an old-style applet, such as storing extra resources inside it; for an example, see "Persistence," later in this chapter. Also, an application bundle can call scripting additions contained within itself; see "Loading Scripting Additions" in Chapter 21. Keep in mind that this format is not compatible with systems earlier than Panther. For applet formats, and for special applet behaviors when an application is missing as an applet starts up, see "Applet and Droplet" and "Application Missing When an Applet Launches" in Chapter 3, and "Using Terms From" in Chapter 19. Persistence works in applets; see "Persistence of Top-Level Entities" in Chapter 8. For the behavior of an applet when a runtime error occurs, see "Errors" in Chapter 19. When an applet runs, no decompilation takes place; for one way this can affect the behavior of your script, see "Raw Four-Letter Codes" in Chapter 20. 27.1.1. Applet OptionsWhen you elect to save a script as an applet, you are given some options that affect how the applet will behave:
27.1.2. Editing an AppletA script saved as an applet is normally still legible and editable. The only way to prevent this is to save the applet as Run Only. Keep in mind that this means even you can't edit the applet's script; if you have no other copy of the script, you lose all ability to edit the applet's script, forever. Let's presume the applet is not run-only. How can its script be edited? Not by double-clicking the applet from the Finder, because that runs the applet. However, a script editor application can still open it (through its Open dialog, for example). In fact, you can even keep an applet script open for editing in a script editor application, save it without closing it, and then double-click it in the Finder to run it, as a way of testing while developing. But, for obvious reasons, you can't save an applet's script into the applet while the applet is actually runningwell, you actually can, but the changed functionality won't be available until you quit the applet and start it up again. Another way to edit an applet is to choose its Edit Edit Script menu item. In a stay-open applet, that menu item can be chosen whenever the applet is idle. In non-stay-open applet, the menus are visible while the startup screen is displayed; thus, the user can start up the applet while holding down the Control key, to force the display of the startup screen, and then choose Edit Edit Script. 27.1.3. Applet Event HandlersAn applet script may contain certain event handlers (see "Event Handlers" in Chapter 9) which, if present, will be called automatically at specific moments in the lifetime of the applet:
So, for example, here's an annoying little stay-open applet: on run display dialog "Howdy!" end run on reopen display dialog "Get to Work!" end reopen on quit display dialog "Farewell!" continue quit end quit on idle beep activate display dialog "Get to Work!" return 1 * minutes end idle An applet is scriptable with respect to its event handlers; that is, you can tell an applet to run, reopen, idle, or quit, and it will execute the respective handler if it has one. If you tell an applet to quit and it has no quit handler, it simply quits. If you tell an applet to idle and it has no idle handler , or to reopen and it has no reopen handler, nothing happens (but the calling script may receive an error). If an applet is not running, do not tell it to run or you'll confuse yourself. The run handler will be called twiceonce because you targeted the applet, and again because you told it to runand the idle handler will be called as well. To prevent this, tell the nonrunning applet to activate (the run handler will be called once, and the idle handler will be called). Alternatively, you can tell the nonrunning applet to launch. In that case, neither the run handler nor the idle handler will be called, and no other event handler is called either; if you immediately tell the applet to quit, not even its quit handler is called. An applet launched in this way "comes to life" only if you send it some further Apple event (for example, tell it to launch and then tell it to run). An idle handler should not be treated as ensuring a precise measure of time; the time interval returned is merely a request not to be called until after that interval has elapsed. (I am not entirely clear on what the time interval is measured from; experiments returning 0 seemed to suggest that it was measured from when the idle handler was last called, not from when it last returned, but this didn't seem to be true for other values.) If your goal is to run a script at certain times or intervals, you might be happier using a timer utility to handle this for you (see "Timers, Hooks, Attachability, Observability" in Chapter 26). A question arises of how to interrupt a time-consuming applet. (This problem, and its solution, were suggested to me by Paul Berkowitz.) Suppose the run handler takes a long time and the user wishes to stop it and quit. The user can press Command-. (period), which raises an error (-128, "User canceled") that stops the run handler dead in its tracks, but the user might not know this. The Quit menu item doesn't work; if the user chooses it, the applet doesn't quitif it has a quit handler, the quit handler is called, but when the quit handler says continue quit, the applet goes right back to executing the run handler where it left off! (I regard this as a bug.) There is also the question of how you can make sure that any required clean-up actions are performed as the applet quits. The best strategy is probably something like this: global shouldQuit global didCleanup on run set shouldQuit to false set didCleanup to false try -- lengthy operation goes here repeat with x from 1 to 10 if shouldQuit then error say (x as string) delay 5 end repeat on error tell me to quit end try end run on quit if not didCleanup then -- cleanup operation goes here say "cleaning up" set didCleanup to true end if set shouldQuit to true continue quit end quit When the user chooses Quit from the menu bar, our quit handler is called, but when we say continue quit we will merely resume the run handler, so we also set a global indicating that the user is trying to quit. The resumed run handler notices this and deliberately errors out, and we catch the error and call our own quit handler, and quit in good order. We would perform our cleanup operations twice in this case, but that is prevented by another global. It's all very ingenious, but it's messy, and ultimately not very satisfactory. If the user chooses Quit from the applet's Dock menu, our applet's quit menu handler isn't called (another bug), and if the user force-quits our applet then of course no cleanup is performed. 27.1.4. DropletsA droplet is an applet onto whose icon the user can drop Finder items (files and folders). Internally, it is simply an applet with an open event handler:
If a droplet is started up by double-clicking it from the Finder, then its run handler is executed and its open handler is not. But if it is started up by dropping items on it in the Finder, then it's the other way around: its open handler is executed and its run handler is not. Once a droplet is running (assuming it is a stay-open droplet), the open handler can be executed by dropping items onto the droplet's icon in the Finder. The open handler is also scriptable, using the open command, whose parameter should be a list of aliases. In this simple example , the droplet reports how many folders were dropped on its icon: on open what set total to 0 repeat with f in what if folder of (info for f size no) then set total to total + 1 end repeat display dialog (total as string) & " folder(s)" end open 27.1.5. PersistencePersistence of top-level entities (see Chapter 8) works in an applet. The script is resaved when the applet quits, maintaining the state of its top-level environment. So, for example, the following modification to the previous example would cause an applet to report the count of folders that had ever been dropped on it, not just the count of folders dropped on it at this moment: property total : 0 on open what repeat with f in what if folder of (info for f size no) then set total to total + 1 end repeat display dialog (total as string) & " folder(s)" end open On the other hand, this persistence naturally ends as soon as the applet's script is edited. If you're still developing an applet, or are likely to edit it further for any reason, you might like a way to store data persistently with no chance of losing it. The application bundle format supplies a solution. An application bundle looks, and behaves in the Finder, just like an applet , but is in reality a folder. We can perform persistent data storage in a separate file inside the bundle; the user won't see this separate file, and its data will persist even when we edit the applet's main script. To illustrate, let's return to the example in "Data Storage" in Chapter 8. This code is just the same as in that example, except that we now assume we are an application bundle, and the opening lines have been changed to store the data inside the bundle: set thePath to (path to resource "applet.icns") as string set text item delimiters to ":" set thePath to ((text items 1 thru -2 of thePath) as string) & ":myPrefs.scpt" script myPrefs property favoriteColor : "" end script try set myPrefs to load script file thePath on error set favoriteColor of myPrefs to text returned of (display dialog "Favorite Color:" default answer "" buttons {"OK"} default button "OK") store script myPrefs in file thePath replacing yes end try display dialog "Your favorite color is " & favoriteColor of myPrefs The first three lines of the script are a rather elaborate song-and-dance to obtain the pathname of the bundle's Resources directory. We could hardcode the path from the top of the bundle, like this: set thePath to (path to me as string) & "Contents:Resources:myPrefs.scpt" Instead, we use the path to resource command. But this command has an odd functionality hole: we can obtain the pathname of a resource file inside the Resources directory, but not the pathname of the directory itself. So we start with a resource file we know is present and work our way up the folder hierarchy and back down again. 27.1.6. Applet ScriptabilityWe have seen already that an applet is scriptable with respect to its event handlers: you can tell an applet to run, open, reopen, idle, or quit, and the corresponding event handler will be called. An applet is scriptable also with respect to user handlers. You call them like ordinary handlers. For example, suppose we have a stay-open applet howdy whose script goes like this: on sayHowdy(toWhom) activate display dialog "Howdy, " & toWhom end sayHowdy Then we can say in another script: tell application "howdy" sayHowdy("Matt") end tell The value is returned as one would expect; here, the calling script receives the value {button returned:"OK"} if user presses the OK button. The reason this is possible is that AppleScript's mechanism for calling a user handler in a script object is extended to work even when you're talking to an applet . The call is translated into a special event, the Call·subroutine command ('ascr\psbr'). This is a sort of meta-command; its parameters are the name of the handler you're calling and the parameters you're passing. At the other end, the target applet unpacks the handler call and performs it within its script. Thus it's just as if you said sayHowdy("Matt") from inside the applet's script. If the script defines such a handler, the result comes back as the reply. If it doesn't, you get a "Can't continue" error message. (This error message is familiar from when you accidentally use this same mechanism to send a user handler call to an ordinary scriptable application, which, as we have seen in Chapter 11 and elsewhere, is all too easy to do.) An applet is also scriptable with respect to its other top-level entities. If an applet has a top-level property x, you can get and set its x. And if an applet defines a top-level script object s, you can refer to s, and you can get and set its properties. In short, an applet behaves like a script object, whose top-level entities you can access in the normal way (see "Top-Level Entities" in Chapter 8). You might think of it as a stored script object that you can target without loading it, and that is automatically persistent without your storing it. Of course, this means that when you're scripting an applet, no special dictionary is involved. An applet doesn't have a dictionary, and it doesn't need one, any more than a script object needs a dictionary. Therefore an applet has no way to publish information about what top-level entities it contains. The user who wishes to script the applet must know in some other way how to do so (by reading the applet's script, or through some other documentation). |