Scriptability: A Bare-Bones Introduction (Part 2)
Kevin C. Killion, Stone House Systems, Inc.



Scriptability:
A Bare-Bones Introduction

(Continued..)



Answering Context Questions:
Object Accessor Routines


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:


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.

Resolving Properties


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.

Resolving Objects


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.

Getting and Setting Properties: Preparation


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.

Getting and Setting Properties: The Real Thing


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.

The Dreaded 'aete' Resource


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.

Counting


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.]

Scripting Alternatives


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.

  1. The English language syntax of AppleScript, which is a boon for easy reading of scripts, can be a barrier to writing of scripts. Use of natural language implies a fluidity of meaning that AppleScript just simply does not support. I looked up the word "SET" in the Oxford dictionary, and it had 194 definitions; in AppleScript, "set" has but one meaning. If we promise the user that AS is natural to use, it becomes hard to explain why "get count of rows, copy it to rowcount" doesn't work. Similarly, all the keywords in "set me to true" are legal, but this sentence is completely wrong in AppleScript.

  2. A simple set of syntax rules in a traditional computer language may be easier to learn than a loose set of English-like structures. For example, BASIC makes no promise of fluidity. To express commands, a fairly simple set of rules do the job just fine.

  3. Involved statements in AppleScript strip away much of the English-like allure, by producing complex run-on sentences that really don't look much like English anymore.

  4. AppleScript would appear to be a poor choice for long programs to do data processing, report generation or numerical calculations, if only due to the sheer verbosity that would be required.

  5. Sources of information about AppleScript are limited, while there are hundreds of books and classes on how to learn BASIC.

  6. Many people already know BASIC. BASIC is a thriving, immensely popular real world language. In the form of Visual Basic, it is arguably the most popular development environment for Windows. Even on the Mac, BASIC is rapidly gaining popularity as the preferred macro language for Microsoft's applications.


(The live presentation of this paper will demonstrate some experimental approaches to scripting using tools other than AppleScript.)

Conclusions


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.

Bibliography


To top of page...

Return to contents...


Copyright ©1996 Kevin C. Killion, Stone House Systems, Inc.

Web page by Bill Catambay
Updated: 22-July-1997