



If you're desperately seeking scripting and recording source code to incorporate into your own MacApp programs, you might want to check out a new product called Developer's C++ Source Code Library: OSL Scripting Components.
This product provides the developer with well over 100 carefully designed and documented classes for scripting, recording, and Apple events communication. This full-fledged library of extensible building blocks also helps you to implement other related features such as the management of client and server applications in computer networks. And all this without having to modify a single line of the original MacApp 3.0.x source code!
Using the classes in the OSL Scripting Components package will radically decrease the time it takes to learn what Apple events really is all about. In fact, to guarantee you the quickest possible learning curve, I designed a couple of classes that produces a Sourcebug-like log window, so that you easily can monitor what's really happening inside your application. The ScriptSender and ScriptServer demos (included in the OSL Scripting Components package) both use log windows to let you see what's happening (see Figure 1). Here's how the OSL Scripting Components can be used to communicate with itself, both over the network (as seen here) and within the same application. Since both the client and the server send and handle standard Apple events according to the Required and the Core suites defined by Apple, they can communicate with any other application that also uses the same protocol, for example the ScriptEditor shown in Figure 2.
Note that the client's log window reports that the menu command has been handled and that the Apple event is sent away to its server destination. A moment later the server application's log window reports that it has handled the Apple event, resolved the object specifiers and reached its final goal: for example to get the data we wanted. Then the server sends back the result to the client, thus producing a new line in the client's log window, confirming the result. The last action in a log window is always autoscrolled and highlighted for maximum convenience.)
Since we don't rely on having to modify MacApp itself, this package should also be compatible, direct out of the box, with the forthcoming MacApp 3.0.2 / 3.1. Also, if Bedrock can't/won't implement scripting in its first release, it should be no great obstacle to convert it to that platform, if necessary.
And, don't forget to recompile MacApp!
Example: If you have posted a TGetBooleanDataFromServer command in the client, it doesn't matter where the server is (it could be the same application as the client or another application on another machine on the network), as long as you have a valid address to that server.
Please note that I whenever I say "server" or "client" in this article (and don't specify it further) this means it's applicable both in a client-side/server-side configuration within one application as well as in a client app/server app setup.
One of the most important issues is the one about letting the server application respond with an error code and error string whenever an error occurs in the server. Just issuing a FailOSErr() is unfortunately not good enough-this doesn't send a reply error to the client. Instead I had to add a couple of extra lines of code on the server side for proper and timely responding to the client.
case cSetCellData: this->DoSetCellData();
OK, that was real easy, I admit. Now let's look at what happens inside the DoSetCellData method.
pascal void DoSetCellData()
{
TOSLObjectDescription* cellDescription =
this->BuildCellDescription();
CStr255 newStringData = GetStringDataFromUser();
// or from somewhere else…
TSetServerCellDataCmd* aSetServerCellDataCmd =
new TSetServerCellDataCmd;
aSetServerCellDataCmd ->ISetServerCellDataCmd
(cellDescription, newStringData );
gApplication->PostCommand(aSetServerCellDataCmd );
}
On the first line I call the BuildCellDescription method, which creates a TOSLObjectDescription object. The TOSLObject-Description object contains a full description of a standard Apple Event Registry cCell object.
On the second line I get the data I want to put into the cell in the server application.
On the third, fourth and fifth lines I create the client command, initiate it and post it to the client application's event queue, which immediately performs the command, thus sending the kAESetDataApple event to the server. We'll look at the details in just a minute.
Since Apple events are verb-oriented (each event is a verb like Create, Delete or Open), it made sense to me to define a set of standard event-sending commands that corresponded to each and every one of these verbs. Each such command could have an object description as a parameter, telling the verb which object it should perform its actions on.
The TOSLObjectDescription is a class descending directly from TObject. As you can see in the more detailed description below, it contains a generic description of an object and its "outer" containers.
If we're describing only one object (for example an application object) we have what's called a simple object specifier: application "ScriptSender". However, if one object is put into another object, we have what's called a complex object specifier: window "Client Log" of application "ScriptSender".
An object specifier can be constructed in different ways. Some of these different ways, called reference forms, are:
Since object specifiers are somewhat hard to handle properly (have you ever seen one of Apple's non-object oriented source code examples?), I decided to build a class around them. The benefits of using a TOSLObjectDescription class are many: easy to create, easy to send, and easy to garbage collect.
TOSLFileBasedDocumentDescription* docDecription;
TOSLTableDescription* tableDescription;
docDecription = new TOSLFileBasedDocumentDescription;
docDecription->IOSLFileBasedDocumentDescriptionByName("my doc");
tableDescription = new TOSLTableDescription;
tableDescription->IOSLTableDescriptionByAbsolutePosition
(docDecription, kAELast);
Use this strategy any time you want, especially when you want to describe an object that's somewhere on the server side but has no "live" object instance on the client side.
Sometimes, however, you may have object instances on the client side that are not of the same class as the objects on the server side. A typical example could be to have a TOSLFile-BasedDocument object in the server application, which you represent in the client application with a TMyDocument object. In your TMyDocument your could define a method called GetDescriptionOfMyServerDocument which would do this:
pascal TOSLObjectDescription* TMyDocument::
GetDescriptionOfMyServerDoc()
{
// Get the doc's name:
CStr255 name; // Assuming your client's doc representation
// has the this->GetTitle(name);
// same name as the TFileBasedDocument
// in the server
//Synthesize the document description:
TOSLFileBasedDocumentDescription* docDecription;
docDecription = new TOSLFileBasedDocumentDescription;
docDecription->IOSLFileBasedDocumentDescriptionByName(name);
return docDecription; // return the description
}
An even better way to do it would be to let the TMyDocument class inherit from TOSLDocument instead of from TDocument and to define a GetDescriptionOfMe method, similar to the one shown in the next section.
pascal TOSLObjectDescription* TOSLObject::
GetDescriptionOfMeAndMyContainers()
{
TOSLObjectDescription* myContainers =
this->GetDescriptionOfMyContainers();
TOSLObjectDescription* meAndMyContainers =
this->GetDescriptionOfMe(myContainers);
return meAndMyContainers;
}
The GetDescriptionOfMe method that gets called above must be overridden by its direct subclass. Here's an example of how I do just that when I define a TOSLTable, which inherits directly from TOSLObject:
pascal TOSLObjectDescription* TOSLTable::GetDescriptionOfMe(
TOSLObjectDescription* theOSLObjectDescription) // override
{
CStr255 theTableName = this->GetName();
TOSLTableDescription* anOSLTableDescription =
new TOSLTableDescription;
anOSLTableDescription>IOSLTableDescriptionByName
(theOSLObjectDescription, theTableName);
return anOSLTableDescription;
}
Using this strategy is very convenient, both in the client and in the server. A typical situation is this: the client prepares an object description from a "live" object and puts the result into an Apple event and sends it away to the server. Normally, the server resolves the request and returns some data without using object descriptions at all. The only events that return typeObjectSpecifiers (which means that you should use a TOSLObjectDescription) are the kAECreateElement and the kAEClone events.
So, sending for example a kAEGetData command to the server in order to retrieve the name of the last table in the document "my doc" is no harder than this:
TGetServerTableNameCommand* aGetServerTableNameCommand;
aGetServerTableNameCommand->
IGetServerTableNameCommand(tableDescription);
class TOSLObjectDescription: public TObject
{
private:
DescType fObjectSpecifierClass;
DescType fPropertyID;
DescType fKeyForm;
AEDesc fKeyData;
AEDesc fObjectSpecifier;
AEDesc fPropertySpecifier;
TOSLObjectDescription* fItsContainer;
public:
// Construction/Destruction:
virtual pascal void Initialize(); // override
virtual pascal void IOSLObjectDescription(
DescType theObjectSpecifierClass,
TOSLObjectDescription* itsContainer);
virtual pascal void IOSLObjectDescriptionByAbsoluteIndex(
DescType theObjectSpecifierClass,
TOSLObjectDescription* itsContainer,
long theIndex);
virtual pascal void IOSLObjectDescriptionByAbsolutePosition(
DescType theObjectSpecifierClass,
TOSLObjectDescription* itsContainer,
DescType theAbsolutePosition);
virtual pascal void IOSLObjectDescriptionByRelativePosition(
DescType theObjectSpecifierClass,
TOSLObjectDescription* itsContainer,
DescType theRelativePosition);
virtual pascal void IOSLObjectDescriptionByName(
DescType theObjectSpecifierClass,
TOSLObjectDescription* itsContainer,
const CStr255& theName);
virtual pascal void Free(); // override
// Access:
virtual pascal TOSLObjectDescription* GetItsContainer();
virtual pascal AEDesc GetObjectSpecifier();
virtual pascal DescType GetPropertyID();
virtual pascal AEDesc GetPropertySpecifier();
virtual pascal void SetObjectSpecifierClass(
DescType theObjectSpecifierClass);
virtual pascal void SetItsContainer(
TOSLObjectDescription* itsContainer);
virtual pascal void SetObjectSpecifier(
AEDesc theObjectSpecifier);
virtual pascal void SetPropertyID(DescType thePropertyID);
virtual pascal void SetPropertySpecifier(
AEDesc thePropertySpecifier);
// Action:
virtual pascal AEDesc DuplicateObjectSpecifier();
virtual pascal AEDesc DuplicatePropertySpecifier();
virtual pascal AEDesc DuplicateKeyData();
virtual pascal void PrepareObjectSpecifierByName(
const CStr255& theName);
virtual pascal void PrepareObjectSpecifierByAbsoluteIndex(
long theIndex);
virtual pascal void CreateObjectSpecifier();
virtual pascal void CreatePropertySpecifier(
DescType thePropertyID);
}
Only three fields are filled with data during the "inner" initialization phase (in the IOSLObjectDescription method) of the object: fObjectSpecifierClass, fPropertyID and fItsContainer. The fObjectSpecifierClass is the object's Apple event registry object class (for example cWindow, cApplication, or cTable). The property ID is the ID of the property we are interested in, for example a pName property. The fItsContainer is this object's outer container. For example, a window description's fItsContainer is very likely an application description.
What about the rest of the fields in the TOSLObject-Description class? Well, they are initialized by the "outer" initialization phase, which is, as seen by the method names, more specific in its nature. For example, the IOSLObject-DescriptionByAbsoluteIndex initialization method requests an absolute index as an extra parameter. Since we specify "absolute index" and supply a value of type long, we can start the actual preparing of the object specifier description. The preparing is (in this case) done in the method PrepareObjectSpecifier-ByAbsoluteIndex, which loads the fields fKeyData with an pure fresh AEDesc of type typeLongInteger and the field fKeyForm gets the constant formAbsolutePosition.
Other examples of "outer" initialization methods are IOSLObjectDescriptionByName, IOSLObjectDescriptionBy-AbsolutePosition, and IOSLObjectDescriptionByRelative-Position.
In the implementation part of IOSLTableDescription-ByAbsoluteIndex you'll only do one thing: call the IOSLObject-DescriptionByAbsoluteIndex method. And the only thing that you do in the IOSLTableDescriptionByName method is to call IOSLObjectDescriptionByName. Here's the class declaration:
class TOSLTableDescription: public TOSLObjectDescription
{
public:
// Construction/Destruction
virtual pascal void IOSLTableDescriptionByAbsoluteIndex(
TOSLObjectDescription* itsContainer, long theIndex);
virtual pascal void IOSLTableDescriptionByName(
TOSLObjectDescription* itsContainer,
const CStr255& theName);
}
and here 's the definition:
pascal void TOSLTableDescription::
IOSLTableDescriptionByAbsoluteIndex(
TOSLObjectDescription* itsContainer, long theIndex)
{
this->IOSLObjectDescriptionByAbsoluteIndex
(cTable,itsContainer,theIndex);
}
pascal void TOSLTableDescription::IOSLTableDescriptionByName(
TOSLObjectDescription* itsContainer, const CStr255& theName)
{
this->IOSLObjectDescriptionByName(cTable,itsContainer,theName);
}
Well, investigating the interface for the TOSLObject might enlighten you a bit (see below). Here you'll see that a TOSLObject always stores a reference to its parent object in the fContainerObject field. Reading the Apple Event Registry gives you detailed information on how you should arrange your object hierarchies in your application. Looking for example at the object cApplication in the Apple Event Registry, this tells you that cApplication has two element classes: cDocument and cWindow. Thus, if you should define your own TOSLWindow (but you don't have to, since it's already supplied with the product), you would know that it should be initialized with an application object in the itsContainer parameter.
class TOSLObject : public TObject
{
private :
TObject* fContainerObject;
public :
// Create / delete
virtual pascal void Initialize();
virtual pascal void IOSLObject(TObject* itsContainer);
virtual pascal void Free();
//Access:
virtual pascal TObject* GetContainerObject();
// TOSLObjectDescription support:
virtual pascal TOSLObjectDescription*
GetDescriptionOfMeAndMyContainers();
virtual pascal TOSLObjectDescription*
GetDescriptionOfMyContainers();
virtual pascal TOSLObjectDescription* GetDescriptionOfMe(
TOSLObjectDescription* theOSLObjectDescription);
//subclass this one
(more)
}
Another important feature of the TOSLObject class is its close relationship with TOSLObjectContainer class. It has three methods which all return a TOSLObjectDescription object. The one that you will be calling yourself in your code is the GetDescriptionOfMeAndMyContainers, which calls the other two methods to construct an up-to-date description of the object in question:
pascal TOSLObjectDescription* TOSLObject::
GetDescriptionOfMeAndMyContainers()
{
TOSLObjectDescription* myContainers =
this->GetDescriptionOfMyContainers();
TOSLObjectDescription* meAndMyContainers =
this->GetDescriptionOfMe(myContainers);
return meAndMyContainers;
}
You will, however, have to override the GetDescriptionOfMe() method, since I cannot decide for you what the "best" description of a generic object should be. Some objects might be better off by being described with an index, and others may want to be described with names instead (or colors, sounds, IDs, whatever). Since I didn't put any extra fields in the TOSLObject class to minimize the overhead, I simply can't describe the object.
Here's my override of GetDescriptionOfMeAndMyContainers from the TOSLTable class:
pascal TOSLObjectDescription* TOSLTable::GetDescriptionOfMe(
TOSLObjectDescription* theOSLObjectDescription)
{
CStr255 theTableName = this->GetName();
TOSLTableDescription* anOSLTableDescription =
new TOSLTableDescription;
anOSLTableDescription->IOSLTableDescriptionByName(
theOSLObjectDescription,theTableName);
return anOSLTableDescription;
}
An alternative in the implementation of the OSL Scripting Components library could have been to define a fName or/and a fIndex field in the TOSLObject class and to provide a fully functional GetDescriptionOfMe method which by default returns for example a description by index.
To give you the best of two worlds, I decided to override TOSLObject with a new class called TOSLSearchableObject, which has these two fields inside. By default I have implemented the GetDescriptionOfMe method, so that it uses its name to describe it. Of course, there's nothing stopping you from overriding this method anyway. But before you do, take a look at TSearchableObject: it works together with a list object and supplies basic functionality of objects in small lists and includes searching and sorting the objects with their name and their index. And since these objects are TOSLObjects, you'll get immediate scriptability. And which application hasn't the need for objects in small lists?
This way, you immediately can enjoy scriptability in your own application, without writing any new lines!
TClientCommand, however, uses some features in MacApp's TApplication class to correctly dispatch the reply that is coming back from the server. In fact, MacApp has defined its own commando-constant called cAppleEventReply to be able to dispatch it in its own way, instead of letting the event "get in the programmer's way" into the DoAppleCommand method. When MacApp receives such an event it tries to match it with the events in TApplication's fPendingReplyList, which is a list of all commands that are sent but not yet received. If the incoming event's keyReturnIDAttr is the same as one in the fPending-ReplyList, then MacApp considers it to actually be the reply.
Another place where they really fit in is when we're administrating the communication in client/server-based systems. These systems tend to be rather complex: all of the commands that are sent between the client and the server can have different numbers and types of sending and replying parameters. So, to be able to create a simple-to-use but yet powerful Apple event communications architecture, I decided to build it around MacApp's TClientCommand and TServerCommand.
The nice thing about TClient-Command and TServer-Command is that they identify the need to administrate the sending and receiving of a command in just one single place. This way, you'll get the benefit of having one method in the class handling the sending parameters and another method in the same class handling the replies.
The TOSLClientCommand is responsible for seeing to it that the command can handle TOSLAppleEvents instead of TAppleEvents.
TOSLClientReplyCommand makes the command into an asynchronous command (if you want that).
The TAbstractOSLDescription uses an objectDescription as a parameter and detaches and frees it when ready.
The TAbstractAEGetDataCommand identifies that it is a kAEGetData core suite command, but leaves it to its subclasses to actually coerce the reply correctly.
TStringAEGetDataCommand gets a string from the server and processes it as a CStr255.
TheGetServerApplicationNameCommand builds a TOSLApplicationDescription automatically and passes along the pName property to the TStringAEGetData command. You should actually use an override of this command to be able to store the retrieved data in a field or wherever is convenient for you.



