Scriptability: A Bare-Bones Introduction (Part 2)
Kevin C. Killion, Stone House Systems,
Inc.
(Continued..)
As we have said, the system's AEResolve procedure can be given a complex specification of a series of containments and a final object and property, and give back to us a reference to a specific application variable or element. Clearly, this has a nice magical feeling to it.
AEResolve works by breaking the resolution task into a series of simpler questions. Each of these questions takes the form, "Within context, identify a particular element". We accept and handle these questions by registering one or more object accessor routines.
The Apple documentation starts off with the assumption that you'll want to have several object accessor routines to handle different cases. I'll simplify things by using only a single accessor function. (Quite frankly, I think an application can survive very nicely, and keep its code more readable, by using only one.)
We install an object accessor with a simple call at initialization time:
err := AEInstallObjectAccessor(typeWildCard, typeWildCard, @MyObjectAccessor, 0, FALSE)
The two "typeWildCard" parameters tell the system that the single MyObjectAccessor routine should be called for all resolution questions.
The accessor routine has this calling sequence:
FUNCTION MyObjectAccessor (desiredClass: DescType; containerToken: AEDesc; containerClass: DescType; keyForm: DescType; keyData: AEDesc; VAR theToken: AEDesc; theRefCon: longint): OSErr;
This calling sequence includes:
- a reference to the container, that is, the context (containerToken)
- the kind of element to be identified within the context (desiredClass). For example, for an accounting system, a context of a "client" may include both "account" and "transaction" elements; we need to know which is being referenced)
- a reference (keyForm and keyData) for how to find the correct element of the desired class
- an AEDesc into which we are to place our "token" referring to the desired application element.
If you do use the single accessor routine strategy, then you'll dispatch different cases of the question yourself. One clear differentiation is between context/property questions and context/object questions. If the system is looking to find a specific property for an object, your accessor is called with keyForm = formPropertyID. In the code listing accompanying this article, we have separate routines to handle accessing properties, and identifying objects contained within other objects:
VAR err: integer; BEGIN IF keyForm = formPropertyID THEN err := PropertyAccessor(desiredClass, containerToken, keyData, aTokenBody) ELSE IF containerClass = typeNull THEN err := AppObjectAccessor(desiredClass, containerToken, keyForm, keyData, aTokenBody) ELSE IF containerClass = 'docu' THEN err := DocObjectAccessor(desiredClass, containerToken, keyForm, keyData, aTokenBody) ELSE err := errAECantHandleClass; IF err = noErr THEN err := AECreateDesc(desiredClass, @aTokenBody, myTokenSize, theToken); MyObjectAccessor := err; END;
Note that if the desired element is contained by the application itself, the containerClass is typeNull; this is the "top" of the containment hierarchy. Also note that if we cannot provide the needed resolution, we must return an appropriate error code, such as errAECantHandleClass. This will let the system give an appropriate friendly message to the user.
Let us now look at the resolution of properties of objects, and then objects within objects.
Our resolution handler, MyObjectAccessor, calls another routine, PropertyAccessor, to resolve a specifier down to a specific property of a specific object. The routine is given a reference to the object, we must return a reference to the property. The question here is of the form, "Tell me how you'd like me to refer to the weight of this package".
Fortunately, given the way we defined tokens, this is extremely easy. We extract the token representing an object. Properties of this object will be referenced with essentially the same token, just by setting isAProperty to TRUE, and setting the propertyCode field. The only complication is when we are asked about properties of the application itself (typically these are global settings or values). In that case, the containing token is 'typeNull', and we must fill in the fields to note that the "object" under discussion is the application.
FUNCTION PropertyAccessor (desiredClass: DescType; containerToken: AEDesc; keyData: AEDesc; VAR theTokenBody: MyTokenType): OSErr; VAR propertyCode: DescType; PROCEDURE Bail (bailErr: integer); BEGIN PropertyAccessor := bailErr; EXIT(PropertyAccessor); END; BEGIN IF containerToken.descriptorType = typeNull THEN { container is the app (which doesn't have a token of its own), } { so make a token for the property } BEGIN theTokenBody.myTokenCode := typeNull; theTokenBody.theObject := NIL; theTokenBody.subReference := 0; END ELSE IF NOT GetTokenFromAEDesc(containerToken, theTokenBody) THEN Bail(eContainerDoesNotHaveValidToken); BlockMove(keyData.dataHandle^, @propertyCode, 4); theTokenBody.isAProperty := TRUE; theTokenBody.propertyCode := propertyCode; PropertyAccessor := noErr; END;
Note that the object is specified with an AEDesc called containerToken. We call a little utility routine GetTokenFromAEDesc, which typically simply extracts one of our tokens from the handle of the AEDesc.
We have been given a code for the desired property in the keyData parameter. We use that value to set our propertyCode field.
Finding an object "contained" within another object requires a bit more effort. However, once you get past the basic requirements of this step, you'll quickly find some wondrous abilities.
Resolving object containment issues involves questions of the form, "On this truck, give a way to refer to the fifth package on your manifest", or, "For this list of customers, tell me how to refer to the client named 'Able & Baker'".
In the "Scripting.p" listing, we have a routine "DocObjectAccessor" which is used to find specific rows within a given document.
As in the property case, we start by extracting one of our tokens from the supplied containerToken. We also confirm that we are being asked for a contained row (coded as 'crow'), which is the only contained element we know about.
IF NOT GetTokenFromAEDesc(containerToken, docToken) THEN Bail(eContainerDoesNotHaveValidToken); IF desiredClass = 'crow' THEN { that's good} ELSE Bail(eContainerDoesNotContainRequestedClass);
In this example, we take the object reference contained in our token, and convert it to a class reference meaningful within our TCL program. The fact that this example used TCL is of small consequence; the crucial notion is that you use whatever you defined as the contents of the token to now identify a particular "object" in your application.
doc := KMyDoc(docToken.theObject);
Now that we have a reference to the document, we will look at a list of rows that happens to have been specified as part of our document. (For TCL fans: In the actual application from which this is extracted, we have our own document class, descended from CDocument, and one of its variables, rowlist, is of class CList.)
rowlist := doc.rowlist; nrow := rowlist.GetNumItems;
We now have an internal reference to the list of rows, and we know how many entries the list has. We now look to see how we are to identify the desired specific row. If keyForm = formAbsolutePosition, that means that the row is to be identified by a serial index. We then extract the desired index from keyData, and check that it is within the valid range.
IF keyForm = formAbsolutePosition THEN BEGIN wantedIndex := LongHandle(keyData.dataHandle)^^; IF keyData.descriptorType = typeLongInteger THEN BEGIN IF wantedIndex <= 0 THEN wantedIndex := nmedia + wantedIndex + 1; END ELSE Bail(errInvalidReference); IF (wantedIndex < 1) | (wantedIndex > nrow) THEN Bail(eIndexNumberOutOfRange); row := KRow(rowlist.NthItem(wantedIndex)); { get desired row, by index} found := TRUE; END ELSE Bail(eOnlyNameIndexFirstOrLast);If the desired sub-element was found, we set the fields of a token accordingly; this is passed back to the system, completing the resolution task.
IF found THEN BEGIN theTokenBody.myTokenCode := rowTokenCode; theTokenBody.theObject := CObject(row); theTokenBody.subReference := 0; theTokenBody.isAProperty := FALSE; Bail(noErr); END ELSE Bail(errAENoSuchObject);
In this brief description, we have only shown how to resolve an element within a container by serial index. With only a little more work (none of it very complex) we add the ability to find elements by name or by keyword (such as "first" or "last"). This adds an exciting pizzazz to the scripting facility, and makes the user's AppleScripts simpler and more lucid.
We won't discuss such enhancements here, but a few samples of these improvements are included in the accompanying listing. Now that you know what "object resolution" is actually about, you'll also find it easier to understand the documentation on this topic in "Inside Macintosh: Interapplication Communication", pages 6-12 to 6-15.
We have now accomplished all of the codework needed to allow the user to examine and set values in the system, with the exception of the actual nitty-gritty: the retrieval or setting itself!
As you'll recall, we referred to a single procedure, DoTransferProperty, in both our Set Data and Get Data handlers. This routine will be used to both set and get property values. We have defined its calling sequence as follows:
FUNCTION DoTransferProperty (propAction: propActionType; VAR myToken: MyTokenType; ae: AppleEvent): OSErr;
The first parameter just sets a direction, either "doSet" or "doGet". (Of course, you indicate this direction with just a Boolean, but I like the self-documenting quality of enumerations.)
The second parameter is the token identifying the property under discussion.
The third parameter is the AppleEvent involved, the incoming AppleEvent for Set Data or the reply AppleEvent for Get Data.
The calls to DoTransferProperty thus looked like this:
{ in the Set Data handler} direction := doSet; err := DoTransferProperty(direction, myToken, theAppleEvent); { in the Get Data handler} direction := doGet; err := DoTransferProperty(direction, myToken, reply);
For full details, consult the complete listings accompanying this article. For now, here are some highlights.
DoTransferProperty begins by pulling out the target object and its desired property from the supplied token. If the object is specified as a class library object or a data handle, it is a good idea to lock the object, since we'll be doing a fair amount of juggling. (We also restore the original lock value when this routine returns.)
obj := myToken.theObject; prop := myToken.propertyCode; oldLock := obj.Lock(TRUE);
The main structure of DoTransferProperty consists of a series of branches to handle the different types of objects that can be handled. If the number of different object types becomes large, you may wish to break this into separate routines, or to methods of the objects' separate classes in a class library. For our simple example we have:
{ APPLICATION} IF myToken.myTokenCode = typeNull THEN BEGIN { - - -} END { WINDOW} ELSE IF myToken.myTokenCode = winTokenCode THEN BEGIN { - - -} END { DOCUMENT} ELSE IF myToken.myTokenCode = docTokenCode THEN BEGIN { - - -} END { ROWS} ELSE IF myToken.myTokenCode = rowTokenCode THEN BEGIN { - - -} END ELSE err := eCannotHandlePropertiesOfThisClass;
Within the BEGIN..END section for each class, we test for each property that we support. If found, we pass the address of the property itself and the relevant AppleEvent to another that does the actual transfer.
As an example, our "row" class has properties that include its name, its height, and various line and fill colors and patterns. We handle these row properties by first coercing our object reference for convenient future use. We then check for each property, calling the "TransferProperty" routine when we find the correct one. Here is an excerpt:
row := KRow(obj); IF prop = '*mht' THEN err := TransferProperty(propAction, @row.height, 'I', SIZEOF(row.height), TRUE, ae) ELSE IF prop = 'flpt' THEN err := TransferProperty(propAction, @row.fillPat, 'I', SIZEOF(row.fillPat), TRUE, ae) ELSE IF prop = 'pppa' THEN err := TransferProperty(propAction, @row.linePat, 'I', SIZEOF(row.linePat), TRUE, ae) ELSE IF prop = 'flcl' THEN err := TransferProperty(propAction, @row.fillCol, 'I', SIZEOF(row.fillCol), TRUE, ae) ELSE IF prop = 'ppcl' THEN err := TransferProperty(propAction, @row.lineCol, 'I', SIZEOF(row.lineCol), TRUE, ae) ELSE IF prop = 'ppwd' THEN err := TransferProperty(propAction, @row.lineThick, 'I', SIZEOF(row.lineThick), TRUE, ae) ELSE IF prop = 'pnam' THEN err := TransferProperty(propAction, PTR(row.title^), 'S', SIZEOF(row.title^^), TRUE, ae) ELSE err := eThisPropertyUnderConstruction;
For example, if the specified property is 'flpt', this is our code for "fill pattern". In our application, we store the fill pattern for a row within the "fillPat" instance variable of an object of type KRow. The AppleEvent that contains the new value (Set Data) or the reply AppleEvent to receive the existing value (Get Data) is specified by ae. The actual transfer may now be performed.
All actual transfers are conducted by a routine we call TransferProperty:
FUNCTION TransferProperty (propAction: propActionType; propPtr: ptr; kind: char; lenProp: integer; writeable: Boolean; ae: AppleEvent): OSErr;
We start by choosing an AppleEvent value type that best fits the property:
IF (kind = 'I') & (lenProp = 2) THEN descriptor := typeShortInteger ELSE IF (kind = 'I') & (lenProp = 4) THEN descriptor := typeLongInteger ELSE IF (kind = 'R') & (lenProp = 4) THEN descriptor := typeShortFloat ELSE IF (kind = 'R') & (lenProp = 8) THEN descriptor := typeLongFloat;
If the direction is "propGet", we must retrieve the value at the location specified, and pack it into the supplied AppleEvent (which is the reply event).
IF propAction = propGet THEN BEGIN { get the value of the specified property} IF lenProp <= SIZEOF(buffer) THEN BlockMove(propPtr, @buffer, lenProp) { copy the contents of the property into the buffer} ELSE BEGIN err := eBufferTooSmall; GOTO 99; END; { stuff the value into the AppleEvent} IF descriptor <> difficult THEN err := AEPutParamPtr(ae, keyDirectObject, descriptor, @buffer, lenProp) ELSE IF kind = 'S' THEN BEGIN strlen := ORD(buffer[0]); err := AEPutParamPtr(ae, keyDirectObject, typeChar, @buffer[1], strlen); END ELSE err := eCannotHandleAPropertyOfThisType; END
On the other hand, if the direction is "propSet", we extract the desired new value from the AppleEvent, and set the property to this new value:
ELSE IF propAction = propSet THEN BEGIN IF NOT writeable THEN BEGIN err := errAENotModifiable; GOTO 99; END; { retrieve the new value from the AppleEvent} IF kind = 'S' THEN { a string} BEGIN err := AEGetParamPtr(ae, keyAEData, typeChar, actualType, @buffer[1], SIZEOF(buffer) - 1, actualSize); IF err = noErr THEN BEGIN strlen := actualSize; IF strlen > 255 THEN strlen := 255; buffer[0] := CHR(strlen); IF (strlen + 1) > lenProp THEN { too big to fit in a string structure this size} BEGIN strlen := lenProp - 1; buffer[0] := CHR(strlen); END; BlockMove(@buffer, propPtr, strlen + 1); END; END ELSE IF descriptor <> difficult THEN BEGIN err := AEGetParamPtr(ae, keyAEData, descriptor, actualType, @buffer, SIZEOF(buffer), actualSize); IF err = noErr THEN BEGIN IF descriptor <> actualType THEN { we didn't get what we wanted} err := ePropertyValueSpecifiedInIncorrectFormat ELSE IF lenProp <> actualSize THEN err := ePropertyValueSpecifiedWithIncorrectSize; END; IF err = noErr THEN { everything looks good, so revise the property itself!} BlockMove(@buffer, propPtr, lenProp); END ELSE err := eCannotHandleAPropertyOfThisType; END;
The beauty of the TransferProperty routine is that it handles all of the get/set needs of this sample within one place. It includes provision for variables of varying types and sizes, and provides a double-check so that "read-only" properties (such as creation date, or whether a window has a title bar) can't be revised.
Believe it or not, we have now concluded all of the coding additions that are necessary to support the vital Set Data and Get Data events. There is only one obstacle remaining before we can say that our application is at least minimally scriptable.
This ugly little beast serves as the translator between how the user talks about the components of your app and how your application discusses those same elements with the scripting calls.
![]()
For example, the user may create a script that refers to a property by the name of "fill pattern". Using the 'aete' resource contained in your application, the system translates this to a code of 'flpt'. When the system asks your application about this property, it will refer to the 'flpt' code.
Unfortunately, the organization of the 'aete' is remarkably convoluted. It organizes the scripting terminology first into "suites" and then lists events, objects and enumerations for each, with properties listed for each object. The complexity of the 'aete' is indicated by the fact that there are only two reasonable ways of creating and editing one, with a resource compiler such as MPW's Rez, or with Resorceror. Apple's own ResEdit tool is not capable of taking on 'aete'.
At this stage, if you've gotten this far in your development, you'll simply want to see scripting happening in your application. As a very simple first cut, you may wish to start with an existing simple 'aete' from another application. The 'aete' that is contained in the Scriptable Text Editor ("STE") sample application from Apple serves as a good foundation.
STE's 'aete' already includes the Get Data and Set Data events, and it includes references to the document object. You may wish to test your scripting features by implementing tests for these document properties. Copy the 'aete' unchanged from the STE into your app. (Of course, it should never go on to users in this form.)
When you successfully have provided access to document properties in this manner, you can then add a few of your own objects. Using Rez or Resorceror, you will need to make these changes:
1) You will need to create entries for the new classes you define, and
2) You will need to identify these new classes as elements within the existing classes that contain them. For example, if you define a class "row" that is contained by the document, then the document class must be revised to show that it now has "row" as one of its sub-elements.
When you have completed a very rough 'aete' resource, you can now test and debug your newly-scriptable application!
At this point, you should pause and seriously review your objectives in scripting and how they would best be handled within your scripting facilities. When you finally get some form of scripting to work, the temptation is strong to plunge ahead and start coding up all kinds of objects and properties. Resist! This is the time to give deep thought to what you want your dictionary to look like to your users. Cal Simone's very fine articles in develop magazine are an excellent source for insights in this area.
There is one final element that pretty much must be included to qualify for a minimal level of scriptability. That is the ability for a script to determine the count of the number of objects there are of a given kind.
[Code to support the two necessary calls for counting is included in the sample code. Time permitting this will be discussed in the live presentation.]
There are many arguments for AppleScript as an ideal scripting language. It would not be a stretch to say that many AppleScript users have crossed over into serious fandom about the language.
One of the great allures of AppleScript is that it looks like English. At the very least, this can help make it easier for people to understand what an existing script is designed to do. While the meaning of "row(3).height = 20" can be learned with some basic programming training, the AppleScript equivalent, "set the height of row 3 to 20", takes no training at all.
In counterpoint to its apparent simplicity, AppleScript is also a real programming language that includes most of the essential constructs one expects. The language includes loops, tests, branches, variables, and mathematical and logical operators ó all the usual goodies.
Despite its allure, its strengths and its small but very dedicated fan following, AppleScript may not be the ideal solution for all users in all situations. The language does have some drawbacks as well.
(The live presentation of this paper will demonstrate some experimental approaches to scripting using tools other than AppleScript.)
Much of the benefit of making your application scriptable can be achieved in a manageable series of short tasks. Once accomplished, this also serves as a sound basis for expanding your support for scripting.
Scriptability is not the same as AppleScript. Making an application scriptable opens the door to control from other scripting languages in the future.
To top of page...
Return to contents...