Uli's Web Site |
|
blog |
Cocoa Scriptability in practiceEven 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. 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. Script Suite DefinitionA 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 TerminologyNow, 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.
|
Created: 2005-01-16 @610 Last change: 2006-03-08 @254 | Home | Admin | Edit © Copyright 2003-2024 by M. Uli Kusterer, all rights reserved. |