Release 1 of the OpenDoc Development Framework introduces support for the creation of scriptable OpenDoc part editors. Since scripting support is new to ODF, and has not been fully qualifed by the ODF quality team, the following caveats apply:
• API related to scripting will change in future releases of ODF
• Quality and reliability has not been verified
• Not all planned scripting, recording, and attaching features are implemented
In spite of these caveats, the current ODF scripting implementation is highly functional and has passed early quality inspections.
Before Getting Started
Before getting started, read Chapter 9 of the OpenDoc Programmer’s Guide, “Semantic Events and Scripting”. This chapter provides an overview of how OpenDoc scripting works, and will provide a reference context for the ODF scripting implementation. This engineering document focuses specifically on the ODF semantic event subsystem and does not attempt to provide a tutorial on scripting support in OpenDoc.
Semantic event support in the Macintosh version of OpenDoc is based on the OpenScripting Architecture (OSA) and the Apple Event Manager. This document assumes a working familiarity with OSA concepts and Apple Event Manager data structures such as AEDescs and AppleEvents. A complete reference on this topic is available as “Inside Macintosh: Interapplication Communication,” published by Addison Wesley. Several helpful journal articles are also listed in the section of this document called “Additional Reading Material.”
Semantic Interfaces
Semantic event support in OpenDoc is based on the OpenDoc semantic interface class. When OpenDoc wants to send semantic events to a part, it requests the part’s semantic interface by calling the part’s AcquireExtension method. Scriptable ODF parts automatically create and return an instance of a semantic interface. The ODF semantic interface understands the object hierarchy of ODF parts, and can walk the object tree to locate specific objects.
Semantic events are targeted at specific scriptable objects that live within a part. When OpenDoc receives a semantic event, it first attempts to locate the object or objects at which the event is targeted. Once OpenDoc has resolved the target specifier into an object or a collection of objects, the event is dispatched to the part that contains the target object. During this process the ODF semantic interface acts as an interface between the OpenDoc shell and the objects contained in the target part.
ODF provides a fully functional implementation of the OpenDoc Semantic Interface. Scripting can be enabled in an ODF part by editing its “Defines.k” file. In its default state, “Defines.k” contains the following two lines:
#define FW_SUPPORTS_EXTENSIONS 0
#define FW_SUPPORTS_SCRIPTING 0
To enable scripting, edit these lines to read:
#define FW_SUPPORTS_EXTENSIONS 1
#define FW_SUPPORTS_SCRIPTING 1
Building a part with FW_SUPPORTS_SCRIPTING defined as 1 causes ODF to automatically create and return an instance of a Semantic Interface when requested by OpenDoc. It is necessary to enable support for extensions since ODF’s scripting implementation relies on extension management features enabled by the FW_SUPPORTS_EXTENSIONS definition.
The ODF semantic interface is implemented in two parts: a SOM wrapper that is defined in the ODF Shared Library, and a C++ implementation class defined in the Semantic Events subsystem of the ODF Framework layer. When ODF creates a semantic interface, it creates both a wrapper and an implementation object. The wrapper class dispatches directly into the implementation object. The wrapper should never be subclassed or modified. By default, ODF creates an implementation object of class FW_CSemanticInterface. If your part requires a customized subclass of FW_CSemanticInterface, do the following:
1. Declare and implement your subclass of FW_CSemanticInterface
2. Add the following line to your “Defines.k” file, replacing “your_custom_class”
with the name of your subclass of FW_CSemanticInterface:
3. Enable scripting and extensions as explained above.
Scriptable Objects
The Semantic Event subsystem of ODF contains a mixin class, FW_MScriptableObject. Classes that mix FW_MScriptableObject acquire the following behaviors: 1) contained “element” objects can be accessed through the containing scriptable object; 2) properties of the object can be accessed and changed by semantic events; and 3) semantic events can be dispatched directly to the scriptable object once object resolution has been completed.
Scriptable subclasses of FW_CPart should mix in one of the two part-specific subclasses of FW_MScriptable. Embedding parts that are also scriptable should mix FW_MEmbeddingPartScriptable. Non-embedding scriptable parts should mix FW_MPartScriptable. Since the ODF semantic interface makes some assumptions about the behaviors of scriptable parts, it is essential that one of the part-specific subclasses be used in preference to FW_MScriptable.
Every scriptable class must have a unique class type constant. AppleScript uses this class type to manage object resolution. When ODF or OpenDoc needs to access the class type of an object, the FW_MScriptable::GetObjectClass method of the object in question is called. GetObjectClass is a virtual method. Subclasses of FW_MScriptable should override this method and return their unique class id.
Scriptable Objects and Frames
Some scripting-related operations require ODF to associate a frame with a scriptable object. Scriptable objects inherit a method called GetFrame from their FW_MScriptable base class that allows them to designate the frame they belong to. By default, this method returns the last active frame of the part. If this default behavior is not appropriate for your scriptable objects, you should override FW_MScriptable::GetFrame and return the correct frame.
Accessing Contained Elements
When OpenDoc receives a semantic event (an Apple event on the Macintosh) it first resolves the target specifier of the event. A target specifier is a tokenized description of a specific object and its relationship to the object hierarchy. In a script, users provide textual descriptions of the objects they want to send events to. AppleScript interprets the textual description, and creates an equivalent tokenized descriptor. An example of an object specifier a user might type into a script is: “shape 1 of part 1”. Object resolution is a multi-stage process in which OpenDoc walks down the object hierarchy looking for the specified object. At each stage of the traversal, OpenDoc calls the part’s semantic interface asking it to find the next contained element.
Classes that mix FW_MScriptable participate in the object resolution process by providing access to the scriptable elements they contain. When ODF wants to access elements of a scriptable object, it calls the GetContainedObject method of the containing object. Parameters passed to GetContainedObject describe the class of the requested object as well as the method used to specify the object and the specification data. GetContainedObject determines the form used to specify the requested object and calls one of the following accessor methods of FW_MScriptable: GetElementByName, GetAdjacentObject, GetElementsWithinRange, or GetElementByAbsolutePosition. GetElementByAbsolutePosition potentially calls one of the following methods of FW_MScriptable: GetFirstElement, GetMiddleElement, GetLastElement, GetAnyElement, GetAllElements, or GetElementByIndex.
FW_MScriptable provides default implementations of all of the object accessor methods listed above. The default implementations of these methods rely on the one access-related method container objects must implement : NewElementIterator. NewElementIterator takes, as a parameter, the object class of the element type ODF is trying to access. NewElementIterator is expected to create and return an instance of a subclass of the abstract base class FW_CElementIterator that iterates over the appropriate objects of the specified class. By implementing this one method, and implementing subclasses of FW_CElementIterator for every scriptable container/element relationship, you enable ODF to provide access to elements via the complete range of specifier forms.
ODFDraw provides an example of how this method is implemented.
iter = FW_NEW(CSemanticShapeElementIterator, (fPartContent));
else
iter = FW_MEmbeddingPartScriptable::NewElementIterator(ev, part, elementType);
return iter;
}
CDrawPart tests the elementType parameter to determine whether or not the type requested is a type contained by Draw. If it is, a shape iterator is created. If it isn’t, the base class is called to provide the iterator. Iterators returned by NewElementIterator must derive from the abstract base class FW_CElementIterator. The First and Next methods of the iterator class return pointers to FW_MScriptable elements.
FW_MScriptable's access of contained objects is always done in terms of element iterators. This default implementation is written to work with any content model and any hierarchy of containers and elements. While this design is flexible and abstract, it is not as optimal as a design implemented with full knowledge of a specific part's content model would be. If your schedule permits, you might want to investigate overriding some or all of the accessors implemented in FW_MScriptable to provide more optimal access based on knowledge of your part's content model.
Properties of Scriptable Objects
Properties of scriptable objects can be tested and set via semantic events. For example, shapes in the ODF Draw example part have a “color” property. The color of a shape can be tested and set from AppleScript scripts as in the following example:
tell application “ODF Draw Document”
-- comment: set the color of the first shape to black
set the color of the first shape to {0, 0, 0}
end tell
FW_MScriptable contains three methods that are called to get and change the property of a scriptable object: HasProperty, GetProperty, and SetProperty. When a scriptable ODF part receives a semantic event requesting a property of a scriptable object, it first calls the object's HasProperty method to verify that the object has the specified property. ODF Draw implements this method in its shape class, as shown below, to indicate that the color property, accessed in the script above, exists:
If the call to HasProperty returns TRUE, ODF calls the SetProperty method of the scriptable object, passing in the information necessary to correctly set the property to the new value. The following example illustrates how this method is implemented for a shape in ODF Draw:
In the example above, CBaseShape::SetProperty first tests to see whether the property being set is the color property. If some other property is specified, the SetProperty method of the FW_MScriptable base class is invoked. If the color is being set, the “part” parameter is dynamically cast from an FW_CPart* to an CDrawPart* (for information on the FW_DYNAMIC_CAST operator, see the ODF documentation on Run-Time Type Identification). Next, the color value is extracted from the propertyValue parameter (see the section in this document on FW_CDesc for an explanation). Once the new color information is extracted, the shape's color is set, and a message is sent to the part's content object requesting a redraw.
ODF accesses property values by calling the GetProperty method of the scriptable object being queried. The following example from ODF Draw illustrates how GetProperty is implemented for a shape's color:
result = FW_MScriptable::GetProperty(ev, part, propertyValue,
whichProperty, desiredType);
return result;
}
Automatic Undo of Set Property
ODF provides automatic support for undo/redo of SetProperty semantic events. The steps ODF follows to implement automatic undo are as follows:
1. Call the HasProperty method of the target object to verify the existence of the specified property.
2. Call the GetProperty method of the target object to get the current property value.
3. Call the GetUndoStrings method of the target object to get the correct undo/redo strings.
4. Create an undoable/redoable command of type FW_CPropertyCommand.
5. Execute the command.
The only method mentioned here that hasn't already been discussed is FW_MScriptable::GetUndoStrings. Scriptable objects can optionally override GetUndoStrings to provide strings that are specific to the property being changed. If a scriptable object does not override this method, default strings of “Undo Set Property” and “Redo Set Property” will be used. Below is an example of how the CBaseShape in ODF Draw overrides GetUndoStrings to provide strings specific to changing a color:
In this example, if the property being set is the color property, undo and redo strings are loaded from the resource fork of the ODF Draw shared library. If the property is not the color property, the FW_MScriptable base class is called to provide the default strings.
Since ODF manages the flow of control through this process, the only responsiblity the part writer has is to implement HasProperty, GetProperty, SetProperty, and, optionally, GetUndoStrings, to enable automatic undo of setting of properties.
Scriptable Collections
Some semantic events are targeted at multiple scriptable objects. One example of this type of event is generated by the use of “whose” clauses in AppleScript. A whose clause specifies a collection of objects that satisfy a specific test or tests. An example of a whose-claused-based directive a user might write when scripting ODF Draw follows:
tell application "ODFDraw Document"
-- comment: set the color of every circle to black
set the color of every shape whose type is circle to {0, 0, 0}
end
OpenDoc and ODF work together to resolve the complex target specifier “every shape whose type is circle”. The result is a collection of scriptable objects, actually CBaseShapes, that satisfy the “type is circle” test. Whenever ODF needs to create a collection of scriptable objects, it creates an instance of the class FW_CScriptableCollection. FW_CCollection is a hybrid class: it is both a collection and a scriptable object. This means that every object in the collection must derive from FW_MScriptable. It also means that FW_CScriptableCollection can behave like a scriptable object: it can provide access to its elements, and receive and handle semantic events. When a semantic event is dispatched to an instance of FW_CScriptableCollection, the collection iterates through its contents dispatching the event to each object in turn.
Descriptor Records
OpenDoc defines a set of classes that are used to pass semantic-event related data into and out of parts. Some of the classes defined by OpenDoc are ODDesc, ODToken, ODAppleEvent and ODObjectSpec. If you are familiar with the AppleEvent Manager of the Macintosh toolbox, you'll note that the classes defined by OpenDoc closely parallel structures defined by the AppleEvent Manager. The OpenDoc classes are, out of necessity, SOM-based equivalents of the AppleEvent Manager versions.
ODF addresses the incompatibility between the OpenDoc and AppleEvent manager versions of descriptor records via a class called FW_CDesc. FW_CDesc can be initialized with either an ODDesc or an AEDesc. Conversion operators exist to provide seamless compatibility with OpenDoc interfaces and with Macintosh toolbox interfaces. In other words, you can pass an FW_CDesc as an AEDesc or an ODDesc and the correct conversions will occur. FW_CDesc uses sophisticated caching to insure that, regardless of how it is passed, data is not stale.
In addition to the FW_CDesc interface, ODF provides C++ style insertion and extraction operators for FW_CDesc. These operators are defined as global functions in the source file “FWDscOpr.cp”. In an example that appears earlier in this document, the following lines of code appeared:
// property value is an FW_CDesc passed in as a parameter
FW_CColor newColor;
propertyValue >> newColor;
This example illustrates how a color can be extracted from an AEDesc using one of the descriptor extraction operators. The color could also be extracted as follows:
FW_CDesc's also contain API to enable them to contain AELists and AERecords. List access is supported via the following methods: GetItemCount, GetDescFromList, and DeleteDescFromList. These methods are self explanatory. To support record access, most methods of FW_CDesc take an optional key parameter. If no key is provided, the default “keyNoKey” parameter is used. This internally defined key is recognized by ODF as an indicator that no key should be used to get or set the specified data.
Apple Events
ODF wraps ODAppleEvents inside of a subclass of FW_CDesc called FW_CAppleEvent. FW_CAppleEvent inherits all of the functionality of FW_CDesc. FW_CAppleEvents have additional API that enables access to AppleEvent attributes. ODF provides built-in accessors for AppleEvent class and ID, as well as for subject attributes.
While ODF Release 1 does not provide explicit support for recording of AppleEvents, FW_CAppleEvent contains a sample method called FW_MakeSetPropertyEvent, which was written to provide some insight into how a factored, recording part might be written. This method is not currently called by the framework, and is present specifically for its value as sample code.
Object Specifiers
The Semantic Events subsystem of ODF contains a utility method used in the creation of object specifiers. When creating an object specifer, ODF users should call the FW_CreateObjSpecifier method in preference to the similarly named toolbox method, CreateObjSpecifier. FW_CreateObjSpecifier is functionally equivalent to CreateObjSpecifier, but eliminates the need for a part to link against the Macintosh shared library called ObjectSupportLib.
Part Terminology
OpenDoc parts publish their scripting terminology the same way that applications do. Terminology for a part is contained in an resource of type ‘aete’. ODF does not do anything explicit to effect this process. Information on how to create a terminology resource for your part can be found in the OpenDoc Programmer's Guide.
Additional Reading Material
Inside Macintosh: Interapplication Communication (Addison-Wesley, 1993)
“Apple Event Objects and You” by Richard Clark, develop Issue 10
“Better Apple Event Coding Through Objects” by Eric M. Berdahl, develop Issue 12
“Designing a Scripting Implementation” by Cal Simone develop Issue 21