AppleScript is a language designed to allow normal users to automate repetitious tasks. When an AppleScript script is run, it usually generates some Apple events. Apple events move between applications by way of the Apple event manager and mach messaging.
You can create and run AppleScript scripts using the application /Applications/ AppleScript/Script Editor
. The following simple script will bring up a new document containing the words “Share the love” in TextEdit. Type it into Script Editor and try it out:
tell application "TextEdit" activate make new document at the beginning of documents set the text of the front document to "Share the love" end tell
As a programmer, I find the AppleScript language to be rather strange and hard to work with, but it has done a great job of helping users to automate common tasks. Instead of writing a script from scratch, I usually search the Internet for one that does almost what I want, and then I tinker with it until it acts in accordance with my wishes.
Executing an AppleScript from a Cocoa application is quite straightforward. If the preceding script was in an NSString
, you could compile and run it like this:
NSAppleScript *appleScript; NSDictionary *errorDict; NSAppleEventDescriptor *ae; appleScript = [[NSAppleScript alloc] initWithSource:theString]; ae = [appleScript executeAndReturnError:&errorDict];
The NSAppleEventDescriptor
produced has the result that was returned at the end of the script. NSAppleEventDescriptor
has three methods for reading the result as standard types:
- (SInt32)int32Value - (NSString *)stringValue - (Boolean)booleanValue
(Boolean
, BOOL
, and char
are all the same thing.)
Making a Cocoa application accessible to AppleScripters is done via key-value coding. Before you begin, you should recognize that the AppleScript system is rather finicky. Your plists must be carefully created, or nothing works. (In fact, an error in your plists can crash Script Editor.)
In Xcode, open the SpeakLine project from Chapter 5.
The first step is to inform the system that your application is AppleScript enabled. The Info.plist
for SpeakLine contains a dictionary; add the following key and value to that dictionary:
<key>NSAppleScriptEnabled</key> <string>YES</string>
By default, your application automatically understands a set of AppleScript commands. To exercise these commands, clean, build, and launch the app. In Script Editor, run a script that uses SpeakLine:
tell application "SpeakLine" activate get name of first window end tell
The title of the main window should appear in the lower pane of Script Editor (Figure 28.1).
To see what attributes, relationships, and commands are available, click the Open Dictionary... menu item and look at SpeakLine's AppleScript dictionary. It should look something like Figure 28.2.
Notice the two suites: Standard and Text. A suite is a collection of AppleScript-able classes and commands. Window
, for example, is a class. Each class has attributes. A window, for example, has a Boolean attribute miniaturized
. Some attributes are read-only; others can be set. Classes also have relationships, which come in two varieties: ToOneRelationship
and ToManyRelationship
. For example, the application class has a ToManyRelationship
with its windows. A window has a ToOneRelationship
with the document it is displaying. Finally, a class has supported commands. For example, a document can handle a Print
command. These ideas are rendered as a diagram in Figure 28.3.
All of these parts are defined in a .scriptSuite
file. The examples mentioned previously are all in /System/Library/Frameworks/Foundation.framework/Resources/NSCoreSuite.scriptSuite
. Open that file in TextEdit and browse it for moment.
Each part in the .scriptSuite
file has an entry in a corresponding .scriptTerminology
file that contains the word used for it in a script, its definition, and any synonyms. To review these components, take a look at /Frameworks/Foundation/Resources/NSCoreSuite.scriptTerminology
in TextEdit.
Thus, the most important part of making your application scriptable is creating good .scriptSuite
and .scriptTerminology
files.
In Xcode, create two new text files in the Resources group: SpeakLine.scriptSuite
and SpeakLine.scriptTerminology
. Add a class with a command and an action to the SpeakLine.scriptSuite
. (The system is very finicky about these files, so type carefully.)
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Name</key> <string>SpeakLine</string> <key>AppleEventCode</key> <string>Spkl</string> <key>Classes</key> <dict> <key>MyApplication</key> <dict> <key>AppleEventCode</key> <string>capp</string> <key>Attributes</key> <dict> <key>utterance</key> <dict> <key>AppleEventCode</key> <string>Utte</string> <key>Type</key> <string>NSString</string> </dict> </dict> <key>Superclass</key> <string>NSCoreSuite.NSApplication</string> <key>SupportedCommands</key> <dict> <key>Utter</key> <string>handleUtterScriptCommand:</string> </dict> </dict> </dict> <key>Commands</key> <dict> <key>Utter</key> <dict> <key>AppleEventClassCode</key> <string>Spkl</string> <key>AppleEventCode</key> <string>Uttr</string> <key>CommandClass</key> <string>NSScriptCommand</string> </dict> </dict> </dict> </plist>
Now edit the SpeakLine.scriptTerminology
file:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Description</key> <string>SpeakLine Script Suite</string> <key>Name</key> <string>SpeakLine Suite</string> <key>Classes</key> <dict> <key>MyApplication</key> <dict> <key>Attributes</key> <dict> <key>utterance</key> <dict> <key>Description</key> <string>The utterance</string> <key>Name</key> <string>utterance</string> </dict> </dict> <key>Description</key> <string>Speakline App</string> <key>Name</key> <string>my application</string> <key>PluralName</key> <string>my applications</string> </dict> </dict> <key>Commands</key> <dict> <key>Utter</key> <dict> <key>Description</key> <string>Speak the utterance</string> <key>Name</key> <string>utter</string> </dict> </dict> </dict> </plist>
Using ParseFileasPropertyList
menu item, make sure that you did not make an error in formatting these plists.
Clean and build your project again. In Script Editor, look at the dictionary for SpeakLine. It should now have a third suite containing your class (with the attribute utterance
) and the utter
command (Figure 28.4).
Your suite claims that you are going to subclass NSApplication
and add the utterance
attribute and a method called handleUtterScriptCommand:
.
In Xcode, create a new Objective-C class called MyApplication
. Declare the method to handle the command in MyApplication.h
:
#import <Cocoa/Cocoa.h> @interface MyApplication : NSApplication { } - (void)handleUtterScriptCommand: (NSScriptCommand *)command; @end
Basically, MyApplication
plans to pass the responsibility off to the AppController
class. How will it get a message to the AppController
? In Interface Builder, you will make AppController
be the delegate of the MyApplication
object.
Open MainMenu.nib
and control-drag from the File's Owner (which represents the instance of MyApplication
) to the AppController
. Set the delegate
outlet as shown in Figure 28.5.
Returning to Xcode, implement the method in MyApplication.m
:
- (void)handleUtterScriptCommand:(NSScriptCommand *)command { NSLog(@"handleUtterScriptCommand:%@", command); [[self delegate] sayIt:nil]; }
Import AppController.h
at the beginning of MyApplication.m
.
What about the utterance
attribute? An application's attributes and relationships can be handled by its delegate. The delegate implements the following method:
- (BOOL)application:(NSApplication *)a delegateHandlesKey:(NSString *)key
For any keys that the delegate can handle, it returns YES
. Add the following method to AppController.m
:
- (BOOL)application:(NSApplication *)sender delegateHandlesKey:(NSString *)key { NSLog(@"Key checked = %@", key); if ([key isEqual:@"utterance"]) return YES; else return NO; }
Next, create accessors for that attribute. Of course, you don't actually have an utterance
instance variable, so the accessors that are called will access the string in the text field:
- (NSString *)utterance { return [textField stringValue]; } - (void)setUtterance:(NSString *)s { [textField setStringValue:s]; }
The final step is to edit the Info.plist
so that it uses your subclass instead of NSApplication
when the application launches. In Xcode, edit the Info.plist
:
<key>NSPrincipalClass</key>
<string>MyApplication</string>
Build and launch your app. In Script Editor, try this AppleScript:
tell application "SpeakLine" activate set utterance to "The rain in spain" utter end tell
The utterance should change, and then you should hear it spoken.
The .scriptTerminology
file can be localized so that scripters can script in their native language.
Creating these plists so that they are consistent with one another and with the CoreSuite is a tricky business. SuiteModeler is a shareware application created by Don Briggs that takes much of the sweat out of creating these plists. A license for it is less than $50. I use SuiteModeler, and it has saved me a lot of frustration.