Mar 02 AppleScript
Volume Number: 18 (2002)
Issue Number: 03
Column Tag: AppleScript
AppleScript Studio: Implementing an Application Preferences System
by Bill Cheeseman, Quechee, VT
Saving and Retrieving Application Preferences in AppleScript Studio
We provided an overview of AppleScript Studio in the first article in this series, AppleScript Studio: An Introduction. In the second and third articles, we will work with Sir Arthur Conan Doyle to solve the mystery of how to create, open and save documents in AppleScript Studio. The existing documentation does not cover this, but an important clue suggesting that it can be done is already in the evidence room: one of the three standard AppleScript Studio templates is named "AppleScript Document-based Application." Although the Watson example that forms the basis of the AppleScript Studio tutorial uses this template, neither it nor any of the other examples actually addresses the mystery. In the course of our detective work, we will therefore help Sir Arthur to write an example application of his own. We will name it "Doyle" in his honor, carrying the current fad of naming applications after Sherlock Holmes characters to its logical conclusion.
The Doyle application will need a preferences file. The first solution an AppleScripter thinks of when the subject of preferences comes up is to use a property. However, the Release Notes for the first version of AppleScript Studio mention that AppleScript properties do not retain changes to their values between relaunches of the application. This is contrary to the normal behavior of AppleScript properties (and, for that matter, of AppleScript global variables, which also retain their values across repeated launches of a script or script application). A future release of AppleScript Studio will presumably restore the persistence of AppleScript properties. In the meantime, the AppleScript Studio Release Notes recommend that "if you want persistent storage of values, write them to a preferences file." We will therefore include in Doyle the ability to create, open and save a preferences file, which provides a simple introduction to the use of documents in AppleScript Studio. The exercise will prove valuable even if properties eventually resume their accustomed behavior, since preference files can be useful in any application of even modest complexity.
We will tackle the mystery of documents in two installments. In the first installment, we will devise an AppleScript-based preferences system in response to the injunction in the Release Notes. In the second, we will expand the example application to handle more complex documents.
Unlike Sir Arthur's novels, the current mystery does not have only one solution. There are many ways to save and retrieve information in AppleScript Studio, as in Cocoa applications generally. This article offers one solution among many.
This article and the next will include complete listings of the AppleScript statements in the Doyle application. In order to keep the scripts as simple as possible, we will make no attempt to deal with internationalization and localization, and we will do no error trapping.
The AppleScript Document-based Application Template
The first step in writing any AppleScript Studio application is to create a new project in Project Builder, using its File -> New Project menu item to select one of three standard templates. Do this now, and choose the AppleScript Document-based Application template. Save our new project under the name "Doyle" wherever you keep your scripting projects; your Documents folder will do.
Before deciding what we need to add to the template, it will be useful to build and run the template as is, without any changes, to see what already works. Do this now by clicking the Build and Run button in Project Builder. An empty window named "Untitled" appears. This is in accord with the Aqua Human Interface Guidelines, which specify that launching an application or bringing a running application to the front (for example, by clicking its icon in the Dock) should open a new, untitled window if a window belonging to the application is not already open, and it should deminiaturize a window if the only open window is currently miniaturized in the Dock.
The new window contains no controls, but our application does have a menu bar, so let's try out some of its menu items. About Application, under the Application menu, brings up an About dialog. Like several of the menu items, the About dialog contains dummy information. We will shortly customize the menu items and the About dialog using Interface Builder, but we won't have to write any scripts to make them work. Help -> Application Help brings up a dialog stating that the application doesn't have any help. We'll leave that alone in these two articles.
The menu that interests us in these articles is the File menu, with its built-in New, Open, Close, Save, Save As, and Revert menu items. Try them out. Choose File -> New; a new empty window appears, named "Untitled 2." Choose File -> Close; the new window closes, leaving the original "Untitled" window in place. Choose File -> Save or Save As; a standard file saving sheet opens with all the usual features, allowing us to give the document a name and select a location where it should be saved. This is getting worrisome, for it is beginning to appear that there may be no mystery for us to solve!
Continuing with the investigation, name the untitled window "Test" and select your Documents folder as the place to save it. Another sheet promptly appears, saying "Couldn't Save Document." "Aha!" says Sir Arthur, "Here's a mystery. Holmes will have to write a script or two to save this document."
Laying the Groundwork
Before figuring out how to save the document, we will have to help Sir Arthur decide why Holmes would want to save it in the first place, by specifying a task that requires saving and retrieving information. Let's have Doyle (the application) save a log documenting the course of the investigation, recording the date and time of every occasion when a user launches or quits Doyle. To make it more useful, we'll also allow the user to open the log and edit it by adding comments to each entry. In case some detectives prefer to let the evidence speak for itself, we'll also give Doyle a Preferences window in which the ability to edit the log can be turned on and off. It is this last feature, the Preferences window, that we will tackle in this article, leaving the Doyle Usage Log to the next installment.
The basics of building an AppleScript Studio application are covered well in the documentation and need not be repeated here except in brief outline.
First, customize the menu bar. With our new Doyle project open in Project Builder, mouse over to the Groups & Files pane on the left side of the project window, selecting the Files tab if necessary to open it. Expand the Resources group, and double-click MainMenu.nib, one of the files that comes with the template. Interface Builder launches and opens the new application's main menu. Following the steps outlined in the AppleScript Studio documentation, change several of the menu items so they refer to "Doyle" instead of "Application." Don't change the name of the Application menu itself, however; it will automatically take on the name of our application when we run it. Don't forget to save frequently and, when you're done, close the MainMenu.nib file by clicking the MainMenu.nib window's close box.
Next, customize the About box. Click Credits.rtf in the Resources group of the Groups & Files pane in the Project Builder window, and type any appropriate information identifying the team that built Doyle.
Finally, click InfoPlist.strings in the Resources Group and provide new values for the four variables you find there, following the model of the dummy strings in the template. Basically, just change "Application" to "Doyle" wherever it appears, and change the name of the author in the copyright notices so that some fake company doesn't get credit for our work.
Designing the Windows
The Doyle Usage Log Window
Although the details of implementing the Doyle Usage Log will be left to the next installment, we will design the main document window now to give us a sense of our application's look and feel. Double-click Document.nib in the Project Builder window's Resources group to open it in Interface Builder. If you don't see the empty document window at first, double-click the Window icon in the Instances pane of the Document.nib window to bring it to the front. Also, for convenience you can open the Info panel now by choosing Tools -> Show Info. It is very helpful to have this panel open all the time; for this reason, an Interface Builder preference exists to Show Info Window at Startup, but it is turned off by default.
With the document window open and selected, use the Attributes pane of the NSWindow Info panel to set the window's title to "Doyle Usage Log." Also, select buttons and checkboxes in the Info panel as needed to set the window's Backing to Buffered, its Controls to enable Miniaturize, Close and Resize, and its Options to enable Visible at launch time, Deferred, and One shot. The meaning of most of these settings is obvious from their names. You will rarely, if ever, need to change the default settings of those whose meaning is not obvious.
From the Cocoa-Data palette, drag a table view into the document window and place it near the top left corner, using the Aqua guides to position it precisely in accordance with the Aqua Human Interface Guidelines. Drag the table view's bottom right resize handle to make the table fill the window to the right Aqua guide and far enough from the bottom to leave room for a row of buttons. There is no Aqua guide to leave room in advance for the buttons; you will have to readjust the table view in a moment, after adding a button. Create a third column in the table view and name the columns, from left to right, "Date," "Action," and "Comments," using techniques described in the AppleScript Studio documentation. Also, size the left column to hold a date and the center column to hold the word "Run" or "Quit." Finally, select checkboxes in the Info panel to set the table view's Selection to Allows Empty Selection, Allows Multiple Selection, and Allows Column Selection, its Scrollbars to Vertical and Horizontal, and its Options to Allows Resizing, Display Column Headers, and Autoresizes Columns to fit. Other items should be deselected, including Allows Reordering because we want the log to remain in chronological order.
Drag a standard push button from the Cocoa-Views palette to a position near the bottom right corner of the document window, lining it up with the aid of the Aqua guides. Name the button "Delete." The button may resize itself to accommodate the length of its new title, so you might have to readjust its position afterwards. You can also now drag the bottom edge of the table view to position it the proper distance above the button, using the Aqua guides that have now become available.
The Preferences Window
Now we are ready to design the Preferences window. Close Document.nib to get it out of the way for now by clicking its close box, saving it if asked. Still in Interface Builder, choose File -> New; the Starting Point dialog opens, if it wasn't already open. Select Empty under the Cocoa topic and click the New button. A new, untitled Interface Builder nib window opens, with the Instances pane selected and containing File's Owner and First Responder icons. For convenience, save it now, before making any changes, by choosing File -> Save As. Give it the name "Preferences.nib" and navigate to save it in the English.lproj subfolder of the Doyle project folder. Leave the checkbox unselected so that the file extension will not be hidden. When a sheet appears, click the Add button to add the new nib file to the Doyle project and target. If this sheet does not appear, you saved the nib file in the wrong place; this can easily be cured by using Project Builder's Project -> Add Files command, preferably first dragging the new nib file into the correct location using the Finder in order to keep all the project files in one place. After it appears in the Groups & Files pane of the Project Builder project window, it is convenient to drag it into the Resources group if it isn't already there. The small checkbox to its left should be selected, indicating that the file will be included in the target when the application is built.
Select the File's Owner icon in the Instances tab of the Preferences.nib window and, in the Attributes pane of the File's Owner Info panel, select NSApplication to make the application object the owner of this nib file. If a dialog appears warning you that something evil will happen, you haven't succeeded in adding the Preferences.nib file to the project. You can proceed with the design of the window anyway, but be sure you remember to add the file to the project later using Project Builder's Project -> Add Files command.
Drag a window icon from the Cocoa-Windows palette into the Instances pane of the Preferences.nib window. A Window icon appears in the Instances pane and an empty window appears on the screen. Using the Attributes pane of the NSWindow Info panel, give the window the title "Preferences" and configure its settings as you did with the document window, with these two exceptions: the Resize checkbox in the Controls area should be deselected, as should the Visible at launch time checkbox.
Finally, drag a checkbox (or "switch," as it is sometimes known in Cocoa) from the Cocoa-Views palette to the top left corner of the Preferences window, and rename it "Show Comments in Usage Log." Then, resize the window to make it smaller, until the right Aqua guide indicates that you have the correct margin on the right and until the bottom edge is as close to the top as you are allowed to drag it. With the checkbox still selected, use the NSButton Info panel to select the Selected checkbox in the Options area. We will set the Show Comments in Usage Log preference to true by default in one of Doyle's scripts in just a moment.
Save the Preferences.nib file but leave it open. We are now ready to start scripting.
Scripting the Application
We will start by scripting the Preferences window, because it reflects a single value that must be read by the main document window before the latter is opened. We will set up the Preferences window and scripts for setting and getting its one preference value first. In subsequent sections, we will take care of coordinating this preference value and the user interface, and we will arrange to save this value to disk in a preferences document and retrieve it when the application is launched.
Managing the Preference Value
We now have a Preferences window that will allow the user, by checking or unchecking a checkbox, to specify whether comments are to be shown in the Doyle Usage Log. Next, we need to provide a means to hold this preference value—true or false— within the application, so that it can be looked up every time the user opens the Log. An easy way to do this would be to rely on the state of the checkbox itself as a record of the state of the preference item, but we won't do it that way. For many reasons, data values should almost always be stored separately from the user interface. Here, we will save the value of the application's single preference item in a disk-based preferences file.
We will not use Cocoa's built-in application preferences system, based on the NSUserDefaults class, but will instead store Doyle's preferences as an AppleScript record using the Read/Write commands implemented in the Standard Additions scripting addition. In the first release of AppleScript Studio, the Cocoa application preferences system is not available to AppleScript. The AppleScript Studio engineering team has informally made available a temporary workaround for using it, in the expectation that AppleScript access to Cocoa's NSUserDefaults class will become a built-in feature in a later release. Even then, however, there may be reasons to prefer a pure AppleScript solution, so we will develop one here.
As the AppleScript Studio documentation very briefly suggests, it is generally a good idea to follow the Model-View-Controller, or MVC, design pattern, whereby the Model (consisting of the application's data and data management algorithms) is kept separate from the View (the application's graphical user interface). The MVC paradigm is particularly important in Cocoa applications, because the Cocoa frameworks are built around the concept and depend upon it in a variety of ways. It may be less important in an AppleScript Studio application, where the event handlers are already laid out according to the AppleScript object model and the structures of a Cocoa application. We will nevertheless follow the MVC paradigm here by storing and manipulating our single preference value in one script, called PreferencesModel.applescript, while coordinating the preference item and the user interface in a second script, PreferencesController.applescript.
If, after reading this article, you adopt this system and create an application of your own having many preference items, you will be thankful that you started out this way. It will allow you to keep all of the preference item data and handlers to manipulate it in one script, while isolating handlers to update the user interface and to obtain information from the user about the preference items in a separate script. Although it isn't necessary to create separate scripts in this manner in AppleScript Studio, getting in the habit of breaking scripts into smaller pieces according to some reasonable plan will help you to keep the scripts manageable as your application grows larger. Here, if you later change the user interface, you will at most have to revise the Controller script; the Model script will continue to work without change. Likewise, if you later rearchitect the data storage strategy, you will at most have to revise the Model script but can leave the Controller script and the rest of the application alone.
Our strategy for implementing these two scripts will be dictated by the fact that AppleScript properties and global variables do not retain their values across relaunches of the application, but the strategy will work as desired even if this problem is repaired in a subsequent version of AppleScript Studio. We will save our preference item to a disk file immediately after it is changed by the user and retrieve it from disk whenever it is needed. Every time the application is launched, it will look for a preferences file in a standard location. If it does not find the file there, it will immediately create a new one, setting its contents to initial default values coded into the application. If it does find a preferences file, it will use the settings found in it, even if the settings it used the last time it was run were different (for example, the user might have substituted a new preferences file for the old file in the meantime).
This strategy will be implemented in a new script, PreferencesModel.applescript. The script will include several handlers for opening the preferences file, closing it, setting its initial default values if the file does not already exist, and retrieving all of its values. In addition, it will include, for the one preference item implemented in this article, a "get" and a "set" handler to retrieve or change its value. Additional get and set primitives can be added later, if additional preference items are needed. These are termed "primitive" handlers, because they get down and dirty with the underlying details of our preferences system. Later, we will call these primitive handlers from higher level handlers implemented in the PreferencesController.applescript, to interact with the user.
We will set the initial, or default, value of our one preference item to true in a permanent property, preferencesDefaultRec, to match the selected state of the checkbox that we created earlier in the Preferences window. The property holds an AppleScript record, and additional labeled fields can be added to it if additional preference items are needed. This default value will be used whenever the application can't find a preferences file. Strictly speaking, it wasn't necessary to select the checkbox in Interface Builder, as we did above, because the application will get the default setting from this property in any event and update the checkbox to match. The property that holds the default preference item value will never be changed by our code, so it will be initialized to the original default value every time the application is launched, and the lack of persistent property changes in this release of AppleScript Studio won't matter. If we change the default, we will recompile PreferencesModel.applescript, and its new value will thereafter remain as is until we again recompile.
As described above, we will provide primitive handlers in PreferencesModel.applescript to initialize and to get and set the values of this property on disk. These handlers are intended to be called from another script in the application, PreferencesController.applescript. However, handlers in another script cannot be called directly in AppleScript Studio. Instead, we have to resort to a simple strategy of indirection. We place each of them inside an explicit script object defined in PreferencesModel.applescript using AppleScript's Script keyword. In fact, the entire body of PreferencesModel.applescript is enclosed in a script object, named preferencesModelLib. Any other script in our application that needs to call one of these handlers will use the Load Script command from the Standard Additions scripting addition to load a copy of PreferencesModel.applescript and store its script object, preferencesModelLib, into a property, prefsModelLib, declared in the calling script. Then the handlers of the loaded script object can be called by telling the property in the calling script to execute any of them.
Declaring a script object within a script, as we have done here, is sometimes required in AppleScript, for technical reasons having to do with the context within which the script's handlers are to be compiled and executed. For a detailed explanation, read the Script Objects chapter of the AppleScript Language Guide and a short article I wrote for The AppleScript Sourcebook, Subroutine Handlers in Script Libraries and Script Servers, at www.applescriptsourcebook.com/tips/scriptserver.html.
To create the new script, select the Preferences window in Interface Builder and go to the AppleScript pane of the NSWindow Info panel. Click the New Script button at the bottom and save it as PreferencesModel.applescript in the Doyle project. New AppleScript scripts can be created in this manner in Interface Builder or, as we will see later, in Project Builder.
Now switch to Project Builder, where you will see the new PreferencesModel.applescript file in the Scripts group. Select it, and, in the editing pane of the project window, type the script object and handlers shown in Listing 1. In AppleScript jargon, this file is called a "script library." The use of script libraries is a common, if somewhat advanced, technique in AppleScript generally. A script library functions much like an "include" file, allowing properties, handlers and other AppleScript constructs declared in one file to be used in another file through AppleScript's Load Script mechanism.
Listing 1: PreferencesModel.applescript
(* PreferencesModel.applescript *)
(* This is an MVC Model script that manages the values of preference settings. It provides an
initialization handler for all of them and get and set accessors for each, as well as some utility
handlers. *)
(* Initial default values are set in the initPreferences() handler, which is executed only if no
preferences file is found at launch. Otherwise, the values of all preferences are obtained from the
preferences file. A preferences file is saved in the current user's Preferences folder at
~/Library/Preferences. The file is always closed immediately after reading or writing in order to
avoid corruption in the event of a crash. Preferences are saved and managed as a single AppleScript
record. Error checking is omitted. *)
(* Properties *)
— Name of preferences file
property preferencesFileName : "Doyle Preferences"
— Default preference record; this property may not be changed by a user, so it serves
— as a permanent record of default values for initialization of new preferences files
property preferencesDefaultRec : {showCommentsPref:true}
— Insert other fields in the preferencesDefaultRec record with comma delimiters
— if additional preferences are added, and recompile.
(* Script Objects *)
script preferencesModelLib
(* Handlers *)
— These handlers provide access to preferences at the most primitive level.
— Load this script object into another script and call these primitive handlers
— wherever access to preferences is required. If the preferences infrastructure
— is later changed, only this script will require revision, so long as the
— names and return values of these handlers remain unchanged.
on openPreferences()
— Opens current user's preferences file, creating an empty file if none exists.
— Returns path to current user's preferences file for use in other handlers.
set preferencesFilePath to ¬
(path to preferences from user domain as string) ¬
& preferencesFileName
open for access file preferencesFilePath ¬
with write permission
return preferencesFilePath
end openPreferences
on closePreferences()
— Closes current user's preferences file, saving changes.
set preferencesFilePath to ¬
(path to preferences from user domain as string) ¬
& preferencesFileName
close access file preferencesFilePath
end closePreferences
on initPreferences()
— Sets preferences to default values if no preferences file exists.
set preferencesFilePath to openPreferences()
try
read file preferencesFilePath
— discard if successful
on error number –39
— end of file indicates file is empty (just created)
write preferencesDefaultRec to ¬
file preferencesFilePath as record
end try
closePreferences()
end initPreferences
on getPreferencesRecord()
— Returns the entire current preferences record.
set preferencesFilePath to openPreferences()
set thePreferencesRec to ¬
read file preferencesFilePath as record
closePreferences()
return thePreferencesRec
end getPreferencesRecord
on getShowCommentsPreference()
— Returns current value of show comments preference item.
set thePreferencesRec to getPreferencesRecord()
return showCommentsPref of thePreferencesRec
end getShowCommentsPreference
on setShowCommentsPreference(setting)
— Sets new value of show comments preference item.
set preferencesFilePath to openPreferences()
set thePreferencesRec to ¬
read file preferencesFilePath as record
set showCommentsPref of thePreferencesRec to setting
set eof file preferencesFilePath to 0
— discard old contents of file
write thePreferencesRec to ¬
file preferencesFilePath as record
closePreferences()
end setShowCommentsPreference
— Insert additional accessors on pattern of getShowCommentsPreference() and
— setShowCommentsPreference(setting) here if additional preferences are added.
end script
The odd character at the ends of some lines in Listing 1 is AppleScript's line continuation character, allowing us to split long lines into shorter segments so they don't run off the page to the right. In most script editors, it is possible to type the line continuation character and start a new line in one stroke by pressing Option-Return. In this release of AppleScript Studio, however, you must type Option-L followed by Return.
After you have typed the body of PreferencesModel.applescript, examine each of its handlers. These are garden-variety AppleScript statements, using the Read/Write commands in the Standard Additions scripting addition. If you aren't familiar with them, you can read about them in Apple's Scripting Additions Guide or any of the several good books about AppleScript currently in print.
Updating the User Interface
Next, we need to write the PreferencesController.applescript script that will call the primitive handlers in PreferencesModel.applescript to get and set the preference item's value, display it in the Preferences window, and accept changes made by the user.
To carry out the Model-View-Controller (MVC) paradigm consistently, we will use this new script to hold the handlers that coordinate the user interface of the Preferences window with the value of the preference item. As its name indicates, it is a Controller, the third component of the MVC trinity. The second component, the View, is essentially already written, in that AppleScript Studio comes with many predefined view classes, such as windows and user controls, with built-in event handlers to take care of their basic functionality. By putting handlers that control the state of the various View objects into a separate Controller script, we effectively isolate the View from the Data. The Controller serves as an intermediary between the Model and the View and therefore must know about both of them. PreferencesModel.applescript, which is the Model holding the preference system's handlers for accessing the data, knows nothing about the nature or features of the user interface (the View), and the built-in Cocoa View objects know nothing about the structure and implementation of the data they represent (the Model). For that reason, PreferencesModel.applescript need not be changed in any way even if we completely revise the user interface. Only the Controller script will need to be customized if the user interface is changed.
Go into Project Builder now and create the new script. Choose File -> New File, then in the New File assistant select AppleScript File under the AppleScript heading and click Next. In the New AppleScript File assistant, name it PreferencesController.applescript and click the Finish button, first making sure that the location is set to the Doyle project folder. If the new file appears elsewhere in the Groups & Files pane of the project window, drag it into the Scripts group. Type the property and script object shown in Listing 2 into PreferencesController.applescript.
Listing 2: PreferencesController.applescript
(* PreferencesController.applescript *)
(* This is an MVC Controller script that contains handlers to coordinate data values in
PreferencesModel.applescript with the user interface in the Preferences window. *)
(* Properties *)
property prefsModelLib : null
(* Script Objects *)
script preferencesControllerLib
(* Handlers *)
— These handlers get and set preferences using plain English terminology, by
— loading and calling the primitive handlers in PreferencesModel.applescript.
— If the preferences infrastructure in PreferencesModel.applescript is later
— changed, this script will not require revision, so long as the names and
— return values of the primitive handlers remain unchanged.
on initPrefs()
loadPreferencesModelLib()
tell prefsModelLib to initPreferences()
end initPrefs
on commentsAreShown()
loadPreferencesModelLib()
tell prefsModelLib to ¬
return getShowCommentsPreference()
end commentsAreShown
on showComments()
loadPreferencesModelLib()
tell prefsModelLib to setShowCommentsPreference(true)
end showComments
on hideComments()
loadPreferencesModelLib()
tell prefsModelLib to setShowCommentsPreference(false)
end hideComments
end script
(* Handlers *)
on loadPreferencesModelLib()
— Loads PreferencesModelLib from PreferencesModel.applescript, if not loaded.
if class of prefsModelLib is not script then
load script POSIX file ¬
((path for script "PreferencesModel" ¬
extension "scpt") of main bundle)
set prefsModelLib to preferencesModelLib of result
end if
end loadPreferencesModelLib
(* Event Handlers *)
— See Listings 5, 6, and 7 for the event handlers in this script.
The AppleScript handlers that are enclosed in an explicit script object named preferencesControllerLib in PreferencesController.applescript are the high-level handlers we referred to above. Their names implement English-like grammar intended to facilitate their use in simple statements that sound very like normal English sentences. Each of them first loads the script object in PreferencesModel.applescript, if it is not already loaded, and then calls the primitive methods implemented there. The high-level handlers are placed in a script object in PreferencesController.applescript so that they, in turn, can be loaded and called by other scripts in the application, as we will see later. While this double layering of handlers is not necessary in an AppleScript Studio application, it is used here as part of our effort to separate the Model object from the View in accordance with the MVC paradigm. It echoes a technique commonly used in Cocoa applications.
Outside of the PreferencesControllerLib script object, PreferencesController.applescript implements one property and one handler. The property, prefsModelLib, will hold the preferencesModelLib script object from PreferencesModel.applescript. It is assigned that value by the handler, loadPreferencesModelLib(), using the Load Script command from the Standard Additions scripting addition.
The loadPreferencesModelLib() handler uses a common AppleScript technique to load the script library only when loading is needed. It first tests the prefsModelLib property to see whether it is of the built-in AppleScript class, Script. If it is, we know that the script library has already been loaded, because the prefsModelLib property was initially set to null in its declaration. This technique takes advantage of the fact that AppleScript properties and variables are not strongly typed; their type is the type of whatever value they currently hold. Therefore, we can call the loadPreferencesModelLib() handler in every handler that gets or sets a preferences value, knowing that the call will do nothing if the script is already loaded, in order to ensure that the script will always be loaded when necessary. As a bonus, if properties retain their values across relaunches of an application in a future version of AppleScript Studio, the script library will only have to be loaded the first time the application is run after it has been compiled or recompiled. (The modest execution efficiency this gives us on subsequent launches comes at the expense of some increase in file size. If this is an issue for your application because its preference settings are very large, you can reset the prefsModelLib property to null when quitting the application).
The file holding the PreferencesModel.applescript script must, of course, be found before it can be loaded in this manner. We know from the AppleScript Studio documentation and from examining built examples that the script files contained in an AppleScript Studio application bundle are located in a Scripts subfolder in the Resources folder in the application bundle's Contents folder. One way to find these scripts, therefore, is to code this path explicitly into a handler. The only other thing we need to know is the path to the running application, which can be obtained from the Standard Additions' Path To Me command. Also, the script's file extension will change from ".applescript" in its text form to ".scpt" in its compiled form. The pathToPreferences() handler in Listing 3, using this technique, is cribbed in part from several of the AppleScript Studio examples.
Listing 3
on pathToPreferencesModel()
set appPath to (path to me from user domain) as text
return (appPath & "Contents:Resources:Scripts:") as text
end pathToPreferencesModel
on loadPreferencesModelLib()
if class of prefsModelLib is not script then
set prefsModelLib to load script file ¬
(my pathToPreferencesModel() & ¬
"PreferencesModel.scpt")
end if
end loadPreferencesModelLib
There is a more flexible means to accomplish the same end, however, using special features in AppleScript Studio's dictionary. The Application Suite in AppleScriptKit.asdictionary includes a Path For event that returns the path to any of several special AppleScript Studio locations, including the location of a script in the application bundle given its name and its compiled file extension. Similar features are provided in the Bundle class, whose properties provide the paths to important components of an application bundle. Here, we use Path For in conjunction with the application object's Main Bundle property, which will hopefully give our application a more robust capability to survive any future changes to the location in which scripts are bundled in some future version of AppleScript Studio.
There is a wrinkle to using the Path For event and the similar class properties: they return folder and file paths using the forward slash character as a delimiter, as required by Cocoa, whereas most AppleScript commands, such as the Load Script command in Standard Additions, require the colon character as a delimiter. Fortunately, AppleScript 1.8, included with the first release of AppleScript Studio, contains a new POSIX File class and a POSIX Path property to make it easy to convert file paths between the slash-delimited POSIX form and the colon-delimited AppleScript form.
A step-by-step version of our loadPreferencesModelLib() handler using the POSIX Path property is set forth in Listing 4. Note that the Path For Script command takes a second parameter, Extension, which is not documented in the manual or the AppleScriptKit.asdictionary. Its usage can be gleaned from one of the examples; using path for script "Preferences.scpt" will not work. Note also that we do not have to bracket Main Bundle in a Tell Application block, because this is assumed in AppleScript Studio. (The final version of our loadPreferencesModel() handler, telescoping the first three lines of the If clause in Listing 4 into a single statement, can be seen in Listing 2.)
Listing 4
on loadPreferencesModelLib()
if class of prefsModelLib is not script then
get (path for script "PreferencesModel" ¬
extension "scpt") of main bundle
get POSIX file result
load script result
set prefsModelLib to preferencesModelLib of result
end if
end loadPreferencesModelLib
Connecting Event Handlers
As written to this point, our scripts will do nothing, because none of the handlers we have written has yet been "connected" to an AppleScript Studio event handler. This is a fundamental lesson to be learned in writing AppleScript Studio applications. No AppleScript handlers will be executed unless they are either directly connected to an AppleScript Studio event handler using the AppleScript pane in the Interface Builder Info panel, or unless they are called (directly or indirectly) from a handler that is connected to an event handler in this manner. This is why, for example, a Run or Reopen handler in an AppleScript Studio script will never be called, even though it will compile because these events are proper AppleScript syntax: there is no Run or Reopen event in AppleScript Studio, so we can't connect them.
Event handlers are connected using Interface Builder. Before switching to Interface Builder to do this, however, we must first save our project in Project Builder. Due to the way the first release of AppleScript Studio is set up, there is a risk that we will lose most or all of our scripts if the aren't saved before switching to Interface Builder and connecting new event handlers. Therefore, compile and save the project now.
The first event handler we will connect is Choose Menu Item in MainMenu.nib. The user of Doyle must have some means of opening our Preferences window in order to review and set preferences, and in Mac OS X this is normally done by choosing the Preferences menu item in the Application menu. In Interface Builder, open MainMenu.nib and, if necessary, double-click the MainMenu icon in the Instances pane to open the menu bar. Click the Application menu on the left to open it, and see that it contains a Preferences menu item. If we were to compile and run the application now, we would find that this menu item is disabled. In order to enable it, we will have to connect an event handler to it. Click on the Preferences menu item to select it, then, in the AppleScript pane of the NSMenuItem Info panel, click Choose Menu Item under the Menu group in the Event Handlers area, click PreferencesController.applescript in the Script area at the bottom, and click the Edit Script button. A new Choose Menu Item event handler appears in PreferencesController.applescript in Project Builder. Fill in the new event handler with the statements shown in Listing 5 (the ellipsis, or three dots, are typed by pressing Option-;).
Listing 5
on choose menu item theMenuItem
if title of theMenuItem is "Preferences..." then
load nib "Preferences"
end if
end choose menu item
Although not necessary at this point, we enclose the Load Nib command in an If block checking the name of the menu item. This will make it easier to connect other Choose Menu Item event handlers in this script in the future, if desired. AppleScript does not allow overloading of handlers, so scripts cannot contain multiple handlers having the same name, even if the direct parameters differ. Being limited to a single Choose Menu Item event handler in any one script, we have to test the parameter in chained If/Else clauses to find the one menu item that the user chose.
This event handler causes the Preferences.nib file to be loaded (notice that the ".nib" file extension is omitted), which will cause the window that we designed in Interface Builder to open. However, it will open invisibly, so we will have to tell it to become visible after the nib file has been loaded. Furthermore, we should set the visual state of its user controls to match the state of the preferences file before making it visible, in order to avoid unsightly flashing. AppleScript Studio invokes a Will Open event handler whenever a window is about to open, so we will connect that event handler next.
Before switching to Interface Builder, save the project file to ensure that our scripts will not be lost. In Interface Builder, open Preferences.nib and, if necessary, double-click the Window icon in the Instances pane to open the Preferences window. Click in an empty area of the Preferences window to select it if something else is currently selected. Then, in the AppleScript pane of the NSWindow Info panel, click Will Open under the Window group in the Event Handlers area, click PreferencesController.applescript in the Script area at the bottom, and click the Edit Script button. A new Will Open event handler appears in PreferencesController.applescript. Fill in the new event handler with the statements shown in Listing 6.
Listing 6
on will open theWindow
if title of theWindow is "Preferences" then
set state of button 1 of theWindow ¬
to preferencesControllerLib's commentsAreShown()
set visible of theWindow to true
end if
end will open
This is the first time we have called a handler that resides in a script object. Here, we do so by qualifying the call with the possessive form of the script object's name, preferencesControllerLib's; we could as well have used AppleScript's alternative syntax, of preferencesControllerLib, or a tell preferencesControllerLib block. The single checkbox in the Preferences window is button 1 of that window (in Cocoa, a checkbox is implemented as a form of button), and we want to set its State property to checked or unchecked, according to the current value of the preference setting. The preference setting is saved as a true or false value in AppleScript terms. Although the Cocoa documentation tells us that a checkbox button's State value is an integer, capable of taking any of the values 1, 0, or -1, Cocoa allows 1 and 0 to be represented, respectively, as YES or NO (true or false). (In Cocoa, -1, or the constant NSMixedState, represents a mixed-state checkbox, portrayed with a dash in the checkbox instead of a checkmark.) We therefore get the return value of the commentsAreShown() handler in the preferencesControllerLib script object and set the State of the checkbox to the returned Boolean value. Since the script object is both declared within and called from PreferencesController.applescript, it isn't necessary to use the Load Script command to load it.
We know, from the work we have done so far, that invoking the commentsAreShown() handler causes PreferencesController.applescript to load the preferencesModelLib script object in PreferencesModel.applescript into the PrefsModelLib property, if it isn't already loaded, and to tell the PrefsModelLib property to execute its GetShowCommentsPreference() primitive handler, and that the latter in turn reads the preferences file from disk, pulls out the value of the desired preference item, and returns it. All of this happens in a trice. Then, when the Visible property of the window is set to true, the window appears on screen, with a checkmark in the checkbox if the preference value on disk was true.
The final event handler needed by PreferencesController.applescript is a Clicked handler to update the value of the preference on disk when the user clicks the checkbox. Cocoa automatically supplies the checkmark when the user clicks. With Preferences.nib still open in Interface Builder, click on the checkbox to select it, then go to the AppleScript pane of the NSButton Info panel. In the Action group, check the Clicked event handler, and check the PreferencesController.applescript script near the bottom, then click the Edit Script button. In Project Builder, fill in the new Clicked handler according to Listing 7.
Listing 7
on clicked theControl
if title of theControl is ¬
"Show Comments in Usage Log" then
if state of theControl is 1 then
tell preferencesControllerLib to showComments()
else
tell preferencesControllerLib to hideComments()
end if
end if
end clicked
By now, you can trace out for yourself exactly how this works. We assume that, if the state of the checkbox is not 1 (true), then it must be 0 (false), because we do not make use of the available third state of a Cocoa checkbox in Doyle.
There is still one important thing missing from our preferences system: the preference file doesn't get initialized to default values if no preferences file is found when the application is launched. We want our preferences to be initialized as soon as the application has finished launching, so an appropriate event handler to connect is the Will Finish Launching event handler in the Application Suite. This is equivalent to Cocoa's awakeFromNib method. It is invoked by the system after the application's nib files have been loaded, the user interface has been initialized, and other Cocoa initialization has taken place.
The MainMenu.nib file is the main nib file for the application, and the File's Owner of that nib file is Cocoa's NSApplication class, so this is where to connect the Will Finish Launching event handler. In Interface Builder, open MainMenu.nib, click the File's Owner icon in the Instances pane to select it, and open the AppleScript pane of the File's Owner Info panel. Expand the Application group and click to select the checkbox beside the Will Finish Launching event handler. Then click to select Application.applescript at the bottom of the File's Owner Info panel, and click the Edit Script button.
When you switch back to Project Builder, you will find that a Will Finish Launching handler has been added to Application.applescript. The Application.applescript script was supplied by the AppleScript Document-based Application template when we first created the project, but it was empty. Type to fill in the statements in the new Will Finish Launching handler as shown in Listing 8.
Listing 8: Application.applescript
(* Application.applescript *)
(* Properties *)
property prefsControllerLib : null
(* Handlers *)
on loadPreferencesControllerLib()
— Loads PreferencesControllerLib from PreferencesController.applescript, if not loaded.
if class of prefsControllerLib is not script then
load script POSIX file ¬
((path for script "PreferencesController" ¬
extension "scpt") of main bundle)
set prefsControllerLib to ¬
preferencesControllerLib of result
end if
end loadPreferencesControllerLib
(* Event handlers *)
on will finish launching theObject
— Initializes the application after main nib files are loaded and initialized by Cocoa.
— Connected to File's Owner in MainMenu.nib
— Initialize preferences to default values, if preferences file not found
loadPreferencesControllerLib()
tell prefsControllerLib to initPrefs()
end will finish launching
You know from earlier work what chain of events this event handler unleashes.
Notice, however, that this is the first time we have called a method in PreferencesController.applescript from outside of that script, in this case, from Application.applescript. We therefore needed a handler to load the preferencesControllerLib script object from PreferencesController.applescript into a property in Application.applescript. We did this the same way we loaded a script property from PreferencesModel.applescript into PreferencesController.applescript. The necessary property and handler are shown in Listing 8.
Until Next Time
This installment is now complete. We have a working preferences system for Doyle.
To check it out, first compile Doyle for deployment. In Project Builder, open the Targets pane by clicking the Targets tab. In the Build Styles area at the bottom, select the Deployment radio button, then rebuild the application. In the Finder, open the Build subfolder in the Doyle project folder, and drag a copy of the Doyle application into your Applications folder. Double-click it to launch Doyle and see its main window open. Then choose Doyle -> Preferences and see the Preferences window open. If this is the first time you have run Doyle, there was no Doyle Preferences file in existence, so you should see that the button is checked to reflect the default value of the preference. Uncheck it and close the Preferences window. Reopen the Preferences window, and you will again see it unchecked, proving that it successfully wrote the new preference value to disk and retrieved it. You can even quit and relaunch Doyle and perform the test again, if you don't believe it.
In the next installment, we will write the necessary scripts to make the Doyle Usage Log work, and to save and retrieve its values in an AppleScript Studio document. We will, of course, use our new preferences system to determine whether the Comments column in the Log window should be shown.
Bill Cheeseman is a retired lawyer now making his living as a Macintosh developer and loving every minute of it. He is uniquely qualified to write about AppleScript Studio, having served as webmaster of The AppleScript Sourcebook (www.AppleScriptSourcebook.com) for many years and having also written a Cocoa tutorial, Vermont Recipes - A Cocoa Cookbook (www.stepwise.com/Articles/VermontRecipes/).