Uli's Web Site
[ Zathras.de - Uli's Web Site ]
Other Sites: Stories
Abi 2000
Stargate: Resurgence
Lost? Site Map!
     home | blog | moose | programming | articles >> blog

 Blog Topics

15 Most Recent [RSS]

 Less work through Xcode and shell scripts
2011-12-16 @600
 iTunesCantComplain released
2011-10-28 @954
 Dennis Ritchie deceased
2011-10-13 @359
 Thank you, Steve.
2011-10-06 @374
 Cocoa Text System everywhere...
2011-03-27 @788
 Blog migration
2011-01-29 @520
 All you need to know about the Mac keyboard
2010-08-09 @488
 Review: Sherlock
2010-07-31 @978
 Playing with Objective C on Debian
2010-05-08 @456
 Fruit vs. Obst
2010-05-08 @439
 Mixed-language ambiguity
2010-04-15 @994
 Uli's 12:07 AM Law
2010-04-12 @881
 Uli's 1:24 AM Law
2010-04-12 @874
 Uli's 6:28 AM Law
2010-04-12 @869
 Uli's 3:57 PM Law
2010-04-12 @867


Cocoa Scriptability in practice

Even taking into account all the complaints I may have about AppleScript as a language (in short: an advanced OO-concept covered under a thin veil of too few coercion handlers that still expose its strong typing, plus an English-like syntax that negates itself by allowing imperative sentences as subordinate clauses and objects), it is still the best way to remote-control your applications. This is mostly due to Apple Events, which are a solid and proven foundation for inter-application communication.

In addition, we have tools like Growl available, and Automator on the horizon with 10.4. Scriptability is becoming more and more interesting to application developers, especially if you're writing small tools that would benefit from Unix-style daisy-chaining to be combined with other apps. And finally, AppleScript is both documented on Apple's web site and available for Cocoa, with XML-based .scriptSuite and .scriptTerminology files replacing the 'aete' resource that required special tools or Rez wizardry.

So much for theory. In practice, the XML files aren't much easier to edit. The best bet right now is using a text editor (like SubEthaEdit), which makes it even more annoying than the Rez files of old. Property List Editor attempts to help with editing, but most of the time just disables the type popups due to a bug. Not to mention it still doesn't support copy & paste and keeps on moving items without taking along the selection. So, if you want to retain your sanity, use a text editor and optionally convert to the ASCII Property List format.

Update: I just heard about SdefEditor, an app that seems to fill the gap of providing an editor for these files. I haven't used it in earnest, but in a cursory test run it looked quite nice.

The docs are another sore point. Basically, they consist of lots of text that tells you how cool AppleScript is and contain so many cross-references you lose track too easily. Generally cross-references are a plus, but in this case it means that it's very hard to read through the docs sequentially in a sensible order.

There's also a number of tables that describe the contents and names of the various entries in the XML files. Sadly, those are sorely missing information on how these relate to each other. You can infer some of it from the names of the tables, but even the names aren't used consistently, and it's easy to miss one of the smaller tables that just tells you what kind of object to wrap another table's fields in.

Also, the samples aren't real-world samples like we had them in Inside Macintosh and other good examples of documentation. Rather, they are unnecessarily long excerpts from the dictionaries for Apple's built-in suites. They are needlessly complex, and since they come standard with the OS, won't be used by anyone but the people who want to replace standard behavior - by definition a minority. Where'd the good old SurfWriter samples go?

Anyway, right now the best way to generate an AppleScript suite is to just take a gander at one of a shipping application and use it as a reference when writing your own one. Mail.app, Adium and Colloquy were rather helpful in this regard. Here's a quick rundown on adding simple scripting support to your app. I'll especially focus on adding commands that aren't bound to any object here because the object-bound stuff is already as sample code on your hard disk and another sample is available at ScifiHifi.

The first thing to do is to turn on scriptability in your app's Info.plist by setting NSAppleScriptEnabled to YES.

On the page that documents this you'll also find info on getting some debugging info you may need later when things start going wrong, and a nice overview of all the techniques and tools available, and how this fits in with MVC.

The executive summary is basically: There are two files. The Suite Definition contains the actual associations of AppleEvents with messages in your Cocoa objects. The Suite Terminology contains the info AppleScript needs to translate from AS-commands to AppleEvents. And scripting stuff ideally goes in the model layer.

Once you've turned on scripting, create your Suite Definition and Terminology files. Pick a name for your suite (typically this will just be the name of your app, e.g. MyApp, MSWord, or whatever...), and use the .scriptSuite suffix for the definition and .scriptTerminology for the AppleScript-to-AppleEvents info.

Script Suite Definition

A suite is divided into several lists. A list of classes, a list of commands, a list of constants etc. So, as an ASCII plist, our SurfWriter.scriptSuite should look like the following:
    "AppleEventCode" = "SfWr";    // Unique identifier for this suite.
    "Classes" = {};        // All your classes.
    "Commands" = {};        // Standalone commands.
    "Enumerations" = {};    // Constants.
    "Name" = "SurfWriter"; // Name of the suite (filename w/o suffix)
    "UsedFeatures" = ( "UnnamedArguments" );
Of course, you'd replace "SfWr" with your application's case-sensitive signature, which you have registered at Apple's Creator Registry. This signature is basically the Classic MacOS equivalent to your application's Bundle ID (and can also be used as a creator code to associate your app's files with your app with finer granularity than file suffixes allow).

Note the "UnnamedArguments" string in the "UsedFeatures" array. This allows you to have commands that don't take a direct argument, and allows you to have commands that take no parameters at all and are shown as such in script editor when your app's dictionary is opened there. Also note that you can leave out any arrays you don't need.

Now, creating classes and commands that work with them is fairly easy with an example to go by. Basically, you just create an entry with the name of the class, specify an (AppleScript) superclass for it and a method to be called on it for each command that is used on it. However, if you want commands that don't work on a particular object, you need to create an NSScriptCommand subclass. To hook up the script command with your class, create a new entry in the Commands dictionary:
    "AppleEventCode" = "SfWr";    // Unique identifier for this suite.
    "Classes" = {};        // All your classes.
    "Commands" =        // Standalone commands.
        "DontPanic" =
            "AppleEventClassCode" = "SfWr";
            "AppleEventCode" = "PNIK";
            "Arguments" = {};
            "UnnamedArgument" = {};
            "CommandClass" = "MyDontPanicCommand";
    "Enumerations" = {};    // Constants.
    "Name" = "SurfWriter"; // Name of the suite (filename w/o suffix)
    "UsedFeatures" = ( "UnnamedArguments" );}
The key is the name which you will use to refer to this command in your ScriptTerminology, so no need to be anally retentive about naming, but pick something you'll remember.

The class code is the same as the suite's code (your app's signature), usually. The Apple Event code is the specific ID of this one command. Together, these two uniquely identify your command, and make sure no other app implements the same command to mean something different. (Though you're free to implement an event defined by another app with its class and ID, to be compatible - but I'd suggest you use a separate suite file for that).

Whenever the { SfWr, PNIK } AppleEvent is sent to this app, a new MyDontPanicCommand object will now be created and its -(id) performDefaultImplementation method will be called. This method can return an object as its result, or nil.

The Arguments dictionary contains a named list of parameters. Each argument has the following form:
"category" =
    "AppleEventCode" = "theC"
    "Optional" = NO
    "Type" = "NSString"
Where "category" is the command's name that you'll use to refer to it in the Terminology. The Apple Event code similarly is an ID identifying this parameter of the command uniquely. If optional is YES, the user may omit this parameter when calling the event (typically your app will use a default value then). And finally, the Type is the Cocoa class to which you want the parameter to be converted. This means passing values between AppleScript and Cocoa is almost hassle-free.

Apart from these labeled arguments, you can also have a direct, unlabeled argument to a command, that comes immediately after the command's name. The inner structure of the unnamed argument is the same as that for a labeled one. The only difference is that instead of naming it and stuffing it into the Arguments dictionary, you put its keys directly into the UnnamedArgument dictionary.

To process any of these arguments in your NSScriptCommand subclass's performDefaultImplementation method, you simply fetch them out of the object's arguments dictionary using the label you specified:
[[self evaluatedArguments] objectForKey: @"category"];
The direct argument has the empty string as its key (@"").

Script Terminology

Now, we need to set up our .scriptTerminology file. The Suite Definition only allows other applications to send Apple Events to our app. But once we've gone through the hassle of setting this up, we might as well go the whole nine yards and allow users to use AppleScript to send us events, too. That's what the Terminology is for, and boy is it easy:
    "Classes" = {};        // All your classes.
    "Commands" =           // Standalone commands.
        "DontPanic" =
            "Arguments" =
                "category" =
                    "Description" = "The kind of panic to be avoided.";
                    "Name" = "in category";
            "Description" = "do not panic";
            "Name" = "do not panic";
            "UnnamedArgument" = {};
    "Enumerations" = {};    // Constants.
    "Name" = "Surf Writer Suite"; // Name of the suite.
Again, we have the familiar list of classes, commands etc. All of that is modeled in parallel to the scriptSuite. The difference is that everything gets human-readable names and descriptions (okay, unnamed arguments obviously don't get a name). The names for the commands and parameters are the keywords and labels that you'll write in AppleScript to invoke this particular command, and they may even consist of multiple words. nifty, eh?

The Suite name and the descriptions are what's displayed when you use ScriptEditor to open a dictionary for your app. That's all it takes to add an AppleScript command to your app.

The command described in the terminology above would, BTW, be written in AppleScript as do not panic in category <string>.

Note: The code here was written in the text editor based on working code from one of my projects for which i had to research this. It may contain typos.

Update: Slight clean-up since this file had lost all its line breaks somehow.

Reader Comments: (RSS Feed)
No comments yet
Or E-Mail Uli privately.
Created: 2005-01-16 @610 Last change: 2006-03-08 @254 | Home | Admin | Edit
© Copyright 2003-2023 by M. Uli Kusterer, all rights reserved.