27.3. Cocoa ScriptingAdding scriptability to an application that you write has been, in the past, not a task for the faint of heart. An 'aete'-format dictionary is difficult to create and maintain. On the programming side, the system needs to be able to call into your application when an Apple event arrives, so as your application starts up it must register the appropriate functions with the Apple Event Manager, and when an Apple event arrives, your code must parse it (no mean feat, especially if it involves a reference to an object in your application) and respond appropriately. For this reason, programmers have often relied on sample code and application frameworks for assistance in making an application scriptable. There was some concern among programmers, therefore, when Mac OS X first emerged, over how it would be possible to take advantage of the Cocoa application framework and make an application scriptable at the same time. Since those early days, support for scriptability has gradually been folded into Cocoa; this is called Cocoa scripting . Cocoa scripting is still not perfect, but at least it has passed its infancy, and in Tiger it is easier than ever, thanks in part to the introduction of the sdef-based dictionary. Thus, if you're a Cocoa programmer, Cocoa scripting in Tiger is a good way to start adding scriptability to your application. Getting started is the hardest part, though, for several reasons:
To help you get started with Cocoa scripting, here's a tutorial that adds the rudiments of scriptability to an existing Cocoa application, a little bit at a time. In order to make the example useful, this scriptability will include elements, properties, an enumeration, and a command. I'll assume you're using Tiger for development, and our example application will be scriptable on Tiger only; once you've achieved Tiger scriptability, it is possible to extend your scriptability to work on earlier systems (I'll talk about how to do that in the next section, "AppleScript Studio Scriptability"). Our Cocoa application, which is called Pairs, is very simple. We have a Person class, and we can create multiple instances of it. A Person has a name. Two Person instances can be paired, and this works like a monogamous marriage: once a Person is united to another, it can't be united to any other Person. The application presumably has some sort of interface, but I'm not going to concern myself with that. We assume that the application's basic functionality is working, and that it initializes itself on startup into some useful statefor example, it creates two initial Persons, named "Jack" and "Jill"and now we want to go back and make it scriptable. Here's the structure of the application before we start adding scriptability. The main controller class, instantiated in the nib, is MyObject. Here is its interface: @interface MyObject : NSObject { NSMutableArray* persons; NSMutableArray* pairs; // outlets go here } // method declarations go here @end The persons mutable array is made up of Person objects; each Person instance, as it is created, is added to this array. Here is the interface for Person:
@class Pair;
@interface Person : NSObject {
NSString* name;
Pair* pair;
}
- (NSString *)name;
- (void)setName:(NSString *)aName;
- (Pair *)pair;
- (void)setPair:(Pair *)aPair;
// other method declarations go here
@end As you can see, we have accessors for our name instance variable. We also have a pair instance variable in the Person class, plus there is a pairs array in MyObject. It happens that the rest of our implementation is as follows. A Pair has two Person pointers, called person1 and person2. To pair two Persons, we make a new Pair object, add it to the pairs array, and point its two Person pointers at the two Persons; we also point the pair pointer of each of these paired Persons at this Pair object. Thus, a Pair and its Persons are double-linked; a Person knows it is paired because its pair instance variable isn't nil, and it can find the Person to which it is paired by looking at its Pair's Persons and finding the one different from itself. The question of whether this way of implementing pairings is a particularly good one is beside the point. What's important is that we do not intend to expose this to the end user. You don't have to show the user everything that goes on behind the scenes! The end user will be thinking in terms of persons, not pairs, and we want our scripting interface to match the user's conceptual thought processes, not to reveal our backstage implementation. So how should our scripting interface look to the AppleScript programmer? Clearly there needs to be a person class, and a person should have a name property. There can be multiple persons, so there should be a persons element. This is not a document-based application, so the only coherent location for the persons element is at the top level of the object modelthat is, it will be an element of the application class. Now we'll create our sdef-format dictionary and add it to the project. The best way to make the dictionary is with the wonderful Sdef Editor application (see Appendix C for this and other Cocoa scripting resources). Let's call the dictionary pairs.sdef. Then to make our application scriptable through this dictionary, we must add the following lines to our project's Info.plist file: <key>NSAppleScriptEnabled</key> <string>YES</string> <key>OSAScriptingDefinition</key> <string>pairs.sdef</string> The first step in creating an sdef is to give it whatever common commands we intend to implement. For example, we want the user to be able to ask how many persons there are, using the count command. This won't be possible all by itself; we have to include the count command in the dictionary. Common commands can be found in the Standard Suite (see "Suites" in Chapter 20), which you can access in Sdef Editor by choosing File Open Standard Suite NSCoreSuite. The idea here is to put NSCoreSuite into the dictionary and then immediately remove from it everything we don't need; in this case, the remaining commands will be just count, delete, exists, and makethe bare minimum needed for working with a collection of persons. (There is no need to include get and set, because they are short-circuited, and we don't need an entry for quit because every application can do that.) Now we make a new suite, which I'll call the Pairs Suite. I like to move the application class into this, and I'll simplify the application class, leaving just the name, frontmost, and version properties, which are implemented automatically. Now we can add the person class with its name property, and give the application class a person element. I assume you can figure out how to work with Sdef Editor, so let's focus on the text version of the result. I'll present it in two parts. First we have the automatically generated Standard Suite: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd"> <dictionary title="Pairs Dictionary"> <suite name="Standard Suite" code="????" description="Common classes and commands for most applications."> <cocoa name="NSCoreSuite"/> <command name="count" code="corecnte" description="Return the number of elements of a particular class within an object."> <cocoa class="NSCountCommand"/> <direct-parameter description="the object whose elements are to be counted" type="specifier"/> <parameter name="each" code="kocl" description="The class of objects to be counted." type="type" optional="yes"> <cocoa key="ObjectClass"/> </parameter> <result description="the number of elements" type="integer"/> </command> <command name="delete" code="coredelo" description="Delete an object."> <cocoa class="NSDeleteCommand"/> <direct-parameter description="the object to delete" type="specifier"/> </command> <command name="exists" code="coredoex" description="Verify if an object exists."> <cocoa class="NSExistsCommand"/> <direct-parameter description="the object in question" type="specifier"/> <result description="true if it exists, false if not" type="boolean"/> </command> <command name="make" code="corecrel" description="Make a new object."> <cocoa name="Create" class="NSCreateCommand"/> <parameter name="new" code="kocl" description="The class of the new object." type="type"> <cocoa key="ObjectClass"/> </parameter> <parameter name="at" code="insh" description="The location at which to insert the object." type="location specifier" optional="yes"> <cocoa key="Location"/> </parameter> <parameter name="with data" code="data" description="The initial data for the object." type="any" optional="yes"> <cocoa key="ObjectData"/> </parameter> <parameter name="with properties" code="prdt" description="The initial values for properties of the object." type="record" optional="yes"> <cocoa key="KeyDictionary"/> </parameter> <result description="to the new object" type="specifier"/> </command> </suite> Next comes the Pairs Suite. The application's name, frontmost, and version properties were generated automatically; the application's person element, and the person class itself, were added by you. The material you've actually had to create is shown here in bold type: <suite name="Pairs Suite" code="pAIR" description="The Pairs suite"> <class name="application" code="capp" description="An application's top level scripting object."> <cocoa class="NSApplication"/> <element description="The persons." type="person"> <cocoa key="persons"/> </element> <property name="name" code="pnam" description="The name of the application." type="text" access="r"/> <property name="frontmost" code="pisf" description="Is this the frontmost (active) application?" type="boolean" access="r"> <cocoa key="isActive"/> </property> <property name="version" code="vers" description="The version of the application." type="text" access="r"/> </class> <class name="person" code="pRSN" description="A person." plural="persons"> <cocoa class="Person"/> <property name="name" code="pnam" description="The person's name." type="text"> <cocoa key="name"/> </property> </class> </suite> </dictionary> Even though only a dozen lines were added by you, there's lots of room to go wrong here. (I speak from experience.) The following points are worth emphasizing:
The reason Cocoa keys in the sdef are so crucial is that Cocoa scripting uses key-value coding to find its way through your code. Key-value coding (or KVC) is an informal protocol that takes advantage of Objective-C's dynamism and introspection. It uses a string as a key to hunt for names among your instance variables and methods. The object model is navigated by way of a path leading down from the application class. At every step of a path, your classes must be KVC-compliant (meaning that the right instance variables or methods are present) or things won't work. In our application, so far, there is just one simple little path. The scriptability framework will start with the application class. It has a person element whose Cocoa key is "persons." So the framework looks in MyObject to see if it is KVC-compliant with respect to to the key "persons." Is it? Yes, because it has an instance variable named persons. That instance variable is an NSMutableArray; that's a built-in class which is itself KVC-compliant. The contents of this NSMutableArray are Person objects; that fits with the Cocoa key for this class, which says that everything in persons should be a Person. And Person is KVC-compliant with respect to "name," because it has a name accessor method and a setName: accessor method. We build our application and run it, and point Script Editor at it, to test our scriptabilityand it doesn't work! For example, we say: tell application "Pairs" get person 1 end tell and we get an error message in the console: [<NSApplication 0x314220> valueForUndefinedKey:]: this class is not key value coding- compliant for the key persons The reason is simple: the very first step in the path is incorrectly set up. As our sdef says, the application class's Cocoa key is "NSApplication." But we want the path to start in MyObject, not in NSApplication. We must not change the Cocoa key for the application class; rather, we need a way to tell the scriptability framework to jump from NSApplication to MyObject as it descends the path. One very simple way to do this is to make MyObject the delegate of NSApplication; you can specify this by a connection in the nib. We must also write some code in MyObject announcing that it, as the application delegate, implements certain keys: - (BOOL)application:(NSApplication *)sender delegateHandlesKey:(NSString *)key { if ([key isEqualToString: @"persons"]) return YES; return NO; } We add that code to MyObject, which is now also NSApplication's delegate. We build and run the application, and we test it in Script Editor; lo and behold, it works! tell application "Pairs" count persons -- 2 name of person 1 -- "Jack" name of person 2 -- "Jill" name of every person -- {"Jack", "Jill"} name of every person whose name ends with "k" -- {"Jack"} exists person "Jack" -- true exists person "Matt" -- false delete person "Jack" count persons -- 1 get name of person 1 -- "Jill" set name of person 1 to "Mannie" name of every person -- {"Mannie"} end tell This shows the advantage of starting with an application framework. We've added to an existing application no more than a couple of lines of code and a dozen lines of dictionary, and presto, we're scripting our application. We can get and set a property; we can count elements, delete an element, and test by property for the existence of an element; we can even use a boolean test specifier. Having achieved this initial intoxicating success, we should consider some improvements and refactoring before proceeding any further:
Here's code that illustrates these points. First, I've changed two of the Cocoa keys in the dictionary: the person element of the application class now has Cocoa key "personsArray," and the name property of the person class now has Cocoa key "personName." These changes will allow our code to respond separately to the messages sent by the scriptability framework. I've moved all the scriptability code into its own file, where it is implemented through categories on the existing classes. I'll present it a piece at a time. First we have a general utility routine implemented as a category on NSObject, because every scriptable class will need it: @implementation NSObject (MNscriptability) - (void) returnError:(int)n string:(NSString*)s { NSScriptCommand* c = [NSScriptCommand currentCommand]; [c setScriptErrorNumber:n]; if (s) [c setScriptErrorString:s]; } @end Observe how to return an error to AppleScript: you fetch the pending command and assign it an error number and, optionally, an error message to accompany it. (See Figure 3-1 in Chapter 3; the system holds out the incoming Apple event to your application like an envelope, from which you read the message and into which you insert any response, whether it's a result or an error.) Next, we have the category on Person: @implementation Person (MNscriptability) - (NSScriptObjectSpecifier *)objectSpecifier { NSScriptClassDescription* appDesc = (NSScriptClassDescription*)[NSApp classDescription]; return [[[NSNameSpecifier alloc] initWithContainerClassDescription:appDesc containerSpecifier:nil key:@"personsArray" name:[self name]] autorelease]; } - (NSString *)personName {return name;} - (void)setPersonName:(NSString *)aName { if ([[NSScriptCommand currentCommand] isKindOfClass: [NSCreateCommand class]]) [self setName:aName]; else [master scripterWantsToChangeName:aName of:self]; } @end The implementation of objectSpecifier allows proper object references to be returned to AppleScript. We must specify the object's container, which in this case is the application class, and we must provide a key ("personsArray") matching the Cocoa key in the dictionary for how this element is accessed from that container. Next we have the scriptability accessors for the name property, now keyed through "personName." A tricky architectural difficulty arises immediately, illustrating why it's so hard to get started with Cocoa scripting. In good object-oriented programming, objects are assigned appropriate tasks. Some objects are just data ("model"); other objects control that data ("controller"). In my application, a Person in MyObject's persons array is just data; it is MyObject that should be responsible for creating and validating a Person. But key-value coding slams into your existing application like a sudden side wind, ignoring your architecture and surprising your code. When the user tries to change the name of an existing person, it is Person's setPersonName: that is called, even though it is MyObject that should decide whether the new name is valid. Accordingly, I've given Person a master instance variable pointing at its creator, which in this case is MyObject; when the user asks to change a person's name, the request is shuttled off to MyObject, which will decide the suitability of the requested change and comply if appropriate. But it gets worse. We have an additional problem when the user says make new person, because at that moment the scriptability framework creates a Person object by calling alloc and init directly on our Person class; any designated initializer is ignored, and MyObject doesn't have a chance to perform initializations or pass judgment. There is no easy way to prevent this (such as saying to the framework, "When you want to create a Person, call such-and-such a method"). Furthermore, if the user's command also says with properties {name:"whatever"}, setPersonName: is called to set the new name. This puts our code in a quandary; there is no master, so there is no one to judge the suitability of the new name. Fortunately, if the user is creating this person (a condition for which we can test, as the code demonstrates), the scriptability framework will send the resulting Person object to MyObject anyway, for insertion into the persons collection. So we set the name as requested, because MyObject will eventually get a chance to pass judgment on this proposed new personand initialize it properly. Now let's talk about the category on MyObject. Here's the first part of it: @implementation MyObject (MNscriptability) - (BOOL)application:(NSApplication *)sender delegateHandlesKey:(NSString *)key { if ([key isEqualToString: @"personsArray"]) return YES; return NO; } - (unsigned int)countOfPersonsArray { return [persons count]; } - (Person *)objectInPersonsArrayAtIndex:(unsigned int)i { return [persons objectAtIndex: i]; } First, we have our same old application:delegateHandlesKey: method. Next, we've implemented our own access to the persons array through the Cocoa key "personsArray"; we will report its size and return an object in it, so that the scriptability framework never gets its hands on the array directly. Here's more of the MyObject category: - (BOOL) canGivePerson:(Person*)p name:(NSString*)name { if (!name || [name isEqualToString:@""]) { [self returnError:errOSACantAssign string:@"Can't give person empty name."]; return NO; } if ([self existsPersonWithName: name]) { [self returnError:errOSACantAssign string:@"Can't give person same name as existing person."]; return NO; } return YES; } - (void) scripterWantsToChangeName:(NSString*)n of:(Person*)p { if ([n isEqualToString: [p name]]) return; // nothing to do if (![self canGivePerson:p name:n]) return; [p setName: n]; } - (void)insertObject:(Person *)p inPersonsArrayAtIndex:(unsigned int)index { if (![self canGivePerson:p name:[p name]]) return; [p setMaster: self]; [persons insertObject:p atIndex:index]; } - (void)insertInPersonsArray:(Person *)p { if (![self canGivePerson:p name:[p name]]) return; [p setMaster: self]; [persons addObject:p]; } First we have a general name-checking routine. If the user wants to assign a person a name, either as part of creating that person or altering the name of an existing person, we report an error to AppleScript if the name is the empty string or matches that of an existing person. Then we have the method that will be called by Person if the user tries to change the name of an existing person: either we return an error or we comply by setting the name. Finally, we have two methods that may be called when the user says make new person; I don't actually know which is called when, but it appears they are both needed for KVC-compliance so I've implemented both. This is indicative of another difficulty that besets the new scriptability programmer. There is no straightforward documentation stating directly what methods will be sought and called, and when, and in what order, and what the scriptability framework will do if it fails to find a method it's looking for (will it default to a different method, or will it throw an error declaring your class not KVC-compliant, or will it return a mysterious error to the script, or what?). So you can never be quite sure what you need to implement and what method the framework may decide to call at any moment. Here you can see me working around this difficulty by implementing the same functionality twice. And I do the same thing in the last part of the MyObject category: -(void)removeObjectFromPersonsArrayAtIndex:(unsigned int)index { [self returnError:OSAMessageNotUnderstood string:nil]; } -(void)removeFromPersonsArrayAtIndex:(unsigned int)index { [self returnError:OSAMessageNotUnderstood string:nil]; } @end That code is to prevent the user from deleting a person. I've implemented two methods that do the same thing because the framework seems to complain on different occasions if I fail to implement either one, and I don't know why; lacking clear documentation, it seems easiest to fall back on a double implementation and move on. With all of that in place, the previous test script still works perfectly. In addition, our application can now successfully return an object reference; and it now responds coherently to the user's attempts to do things we don't permit: tell application "Pairs" get name of every person -- {"Jack", "Jill"} delete person "Jack" -- error: Pairs got an error: person "Jack" doesn't understand the delete message make new person -- error: Pairs got an error: Can't give person empty name make new person at end with properties {name:"Moe"} -- person "Moe" of application "Pairs" set name of person "Jill" to "Mannie" set name of person 1 to "Mannie" -- error: Pairs got an error: Can't give person same name as existing person make new person with properties {name:"Mannie"} -- error: Pairs got an error: Can't give person same name as existing person end tell Now let's add some more features. Let's give a person an additional property: gender, which is either male or female. This is simply to illustrate how you implement an enumeration. The Person class will need an instance variable, gender, whose value is an int, and we should probably add accessors gender and setGender. To define the enumeration and its enumerators for AppleScript, you want something in the dictionary like this: <enumeration name="genders" code="gEND" description="A gender." inline="2"> <enumerator name="male" code="gMAL" description="Male gender."/> <enumerator name="female" code="gFEM" description="Female gender."/> </enumeration> Back in our Objective-C code, we define the same enumeration, like this: enum { MALE='gMAL', FEMALE='gFEM' } genders; The match between the four-letter codes in the dictionary and our Objective-C code is crucial. Now, in the dictionary, we add the property to the person class: <property name="gender" code="gNdR" description="The person's gender."> <cocoa key="personGender"/> </property> (Observe that I do not make the common mistake of giving the property the same four-letter code as the class.) The Cocoa key "personGender" means that in our Person class the accessors personGender and setPersonGender: will be called. Implementation of these in the category on Person is straightforward; in my implementation I've allowed (and indeed required) the user to supply a gender when creating a person, but I've made it illegal for the user to change the gender of an existing person. Now let's implement a verb. Let's call this pair, and we'll have it apply to two person objects. One will be the direct object; the other will appear after a parameter, to: pair person 1 to person 2 Verbs (commands) are implemented in two different ways in Cocoa scripting. If a verb basically applies to a single object, it can appear in the Objective-C code as a method in the class that corresponds to the class of that object. This is called object-first dispatch. (The other way of implementing a command, verb-first dispatch, is demonstrated later in this chapter.) Having defined the command in the dictionary, you then specify in the dictionary every class that can serve as the direct object to this command. So the dictionary will contain this definition of the command: <command name="pair" code="pAiRpAiR"> <direct-parameter description="One person." type="person"/> <parameter name="to" code="othR" description="The other person." type="person"> <cocoa key="otherPerson"/> </parameter> </command> And in our person class, the dictionary now contains the following: <responds-to name="pair"> <cocoa method="scripterSaysPair:"/> </responds-to> What this means is that when the user invokes the pair command, a message scripterSaysPair: will be sent to the Person object who represents the direct object of the command. The parameter to this method is an NSScriptCommand object whose evaluatedArguments method yields an NSDictionary containing the command's additional parameters, accessible through their Cocoa keys; in this case, there is just one additional parameter, and its key will be "otherPerson." So now we can implement scripterSaysPair: in our category on the Person class: - (void)scripterSaysPair:(NSScriptCommand*)command { Person* p1 = [command evaluatedReceivers]; Person* p2 = [[command evaluatedArguments] valueForKey:@"otherPerson"]; if (self != p1 || self == p2) { [self returnError:errOSACantAssign string:@"Invalid pairing."]; return; } [master scripterWantsToPair:p1 with:p2]; } After an error check, the command is passed on to MyObject for processing. The routine in MyObject (not shown here) does some more error-checking (making sure neither of the person objects is already paired) and then does whatever it usually does to pair two Persons. I use this architecture in order to distribute responsibilities appropriately; a Person can look to see whether the pair command makes basic sense, but it is MyObject, as master of the persons collection, who decides whether two Persons can be paired and, if so, pairs them. It would be nice to complete the picture by adding a read-only boolean paired property to the person class, stating whether the person has been paired, along with a read-only partner property that returns a reference to the other person in the pair. These correspond to no instance variable of the Person class, which just goes to show that a sensible scripting interface needn't be like the underlying implementation. - (id) personPartner { Pair* myPair = [self pair]; if (!myPair) return [NSNull null]; return ([myPair person1] == self ? [myPair person2] : [myPair person1]); } - (void) setPersonPartner:(id)newPartner { [self returnError:errOSACantAssign string:@"Partner property is read-only."]; } - (BOOL) personPaired { return ([self pair] != nil); } - (void) setPersonPaired:(BOOL)newPaired { [self returnError:errOSACantAssign string:@"Paired property is read-only."]; } The personPartner method returns an NSNull instance if the person hasn't been paired; that's how you cause missing value to be returned to AppleScript. Finally, observe that you must implement setter accessors even though the dictionary marks these properties as read-only. |