September 94 - BUILDING AN OPENDOC PART HANDLER
BUILDING AN OPENDOC PART HANDLER
KURT PIERSOL
OpenDoc, Apple's compound-document architecture, brings users a new, more
powerful metaphor for working with documents. Writing code to support OpenDoc is a
lot like writing a normal application. This article gives an overview of what's involved
in writing OpenDoc code and presents a simple working example.
OpenDoc provides a new way to write application code for the Macintosh and a number
of other desktop platforms. By following the OpenDoc guidelines, you can produce
applications that share files, windows, and interface elements seamlessly. The process
of writing an OpenDoc application, which we call apart handler , is much like writing
any Macintosh application. There are differences as well, of course, and this article
will help you understand them.
OpenDoc applications are designed to allow code from several sources to cooperate in
producingcompound documents , documents that can embed almost any kind of content
inside them. Each piece of content in the document (eachpart ) includes its own part
handler, the code that's used to edit and view it. To achieve this, OpenDoc part handlers
must cooperate in a number of ways. They must sort out how events are passed, where
data is stored on the disk, and where drawing is allowed to occur on windows or printed
pages.
This article starts with a brief overview of OpenDoc and then talks about implementing
a simple part handler. It will show you the absolute basics, much as TESample does for
TextEdit in the Macintosh Toolbox. You'll learn about a simple example of building a
part handler, included on this issue's CD: a clock that can handle two different display
modes, digital and analog. The clock updates itself every second and allows the user to
select the display mode from a menu.
A quick caveat: The sample code provided on the CD is from the alpha version of
OpenDoc, but by the time you read this, a beta version should be available. When you
begin implementing your own part handler, you may find that some details of the API
have changed; however, the overall structure will be the same. The sample clock, for
instance, is specific to C++ and the alpha version of OpenDoc. The final version of
OpenDoc will be based on IBM's System Object Model (SOM), which will allow part
handlers to be written in a variety of languages, both object-oriented and procedural.
Similarly, theXMP prefix on OpenDoc class names (you'll see a lot of them in this
article) will be changed toOD beginning with the beta release. This is perhaps a good time to mention a bit more about SOM. This technology is the
basic mechanism that OpenDoc part handlers use to communicate with one another.
SOM solves many hard problems associated with using object-oriented languages,
including those of subclassing across language boundaries, altering base classes under
dynamic linking, and long-term maintenance of object-oriented APIs.
OVERVIEW OF OPENDOC
Before getting into the specifics of OpenDoc and how to write part handlers, we'll talk
about some of the basic services you'll see in OpenDoc and where your own code fits
into the OpenDoc architecture.
PART HANDLERS
Part handlers are what provide OpenDoc with its ability to handle different kinds of
content in a single document. You, the developer community, will write the various
part handlers that plug into OpenDoc.
Part handlers are a lot like existing applications. They handle events, draw and print,
and read and store data onto disk. Every part handler provides a series of entry points
that allow OpenDoc to request any of these actions from the part handler. In addition,
the API has a number of "bookkeeping calls," which allow OpenDoc to provide undo
services and notify part handlers when their environment has changed.
Overall, there are about 50 calls in the OpenDoc part API that a part needs to
implement. This is a lot, but it actually maps fairly closely with the number of things
you'd have to do to write any Macintosh application. In addition, you can ignore many of
these calls in many cases. For instance, if you don't allow embedding of other parts
within your part, there are about ten calls that you can safely ignore. If you don't
update your display asynchronously, but simply wait for update calls, there are
additional calls that you can ignore. In many typical cases, this means that you can
build a part handler very quickly from existing code.
Part handlers are packaged as shared libraries in the Macintosh version of OpenDoc.
This won't always be the case on other OpenDoc platforms, but you can count on the API
being the same on all platforms. The alpha version of OpenDoc uses the Apple Shared
Library Manager to dynamically link your part handler into OpenDoc, while the beta
version will use SOM. These versions will have different linker behavior but will
essentially require the same basic packaging of your code: a shared library.
In either case, you'll find that OpenDoc is an object-oriented API. That means you'll be
talking to OpenDoc objects, and your part handler will itself be an OpenDoc object (or
set of objects). This doesn't mean that your code has to be built from the ground up in
C++, though. SOM will provide interfaces to many languages, including C.
Because part handlers are themselves objects, we often refer to them as "part objects
or "parts" in conversation. In fact, what the user would call a "part" in a document is
really the combination of some persistent data stored in the document file and a set of
objects that OpenDoc uses to display and manipulate the stored data. OpenDoc chooses
appropriate part handlers based on the type of data stored in the document.
RUNTIME OBJECTS
As we describe how to write a part handler, we'll mention some runtime objects that
interact with your code. In OpenDoc, these objects can be located at run time using the
session object (XMPSession), to which your part object will be given a pointer when
it's initialized. The session object is very important because it's your link to the rest
of the OpenDoc objects that are running in the document.
There's a whole list of objects that the session object makes available. Of these, only
three will be important for the purposes of this article: the arbitrator, the
dispatcher, and the undo stack.
• The arbitrator is an object of class XMPArbitrator. The arbitrator for a
session is the place where part handlers register their ownership of certain
resources. The menu bar, the keystroke stream, and the current selection are
all examples of resources that the arbitrator tracks.
• The dispatcher is the object that dispatches events to the various part
handlers. It's an object of class XMPDispatcher. It's used in our example as a
way to register for background time.
• The undo stack is an object of class XMPUndo that allows OpenDoc to
support multilevel undo across part handler boundaries.
Each of these objects will be discussed as it's encountered.
A RUNNING START
To give you a running start, we've built a small object-oriented framework for parts
that implements the direct interface to OpenDoc. This framework is a precursor to the
new part handler framework that Apple is building, and is included here simply as
sample code. Our sample clock uses this framework. The good thing about the
framework is that it clearly separates the work that any part handler must do to be
OpenDoc compliant from the specific work performed in putting up a clock.
The framework divides the work of a part into three objects: a frame object, a facet
object, and a part object. OpenDoc itself doesn't require that you create anything but a
part object, but for the sake of clarity the framework divides the labor among several
smaller objects. For easy reference, here's a list of the classes we'll be discussing
throughout the article and their corresponding source files, included on the CD:
CPart FWPart.h, FWPart.cpp
CFrame FWFrame.h, FWFrame.cpp
CFacet FWFacet.h, FWFacet.cpp
CClockPart ClockPar.h, ClockPar.cpp
CClockFrame ClockFra.h, ClockFra.cpp
CClockFacet ClockFac.h, ClockFac.cpp
The classes defined by the framework generally start with the letterC , hence the classes CPart, CFrame, and CFacet. These three parts are helper objects for three
OpenDoc classes, XMPPart, XMPFrame, and XMPFacet. XMPPart objects are OpenDoc
part handlers: you'll subclass XMPPart when writing your own. OpenDoc uses the
frame and facet objects to help part handlers lay themselves out in a window. How
these classes work together is probably the single most complex thing to understand in
OpenDoc.
XMPPart, the class from which part handlers are derived, is simply the base class of
every OpenDoc part handler. It's the class that actually handles the drawing, editing,
and storage. Every part handler is an implementation of some subclass of XMPPart.
CPart, in the framework, is a class derived from XMPPart. It's just a default
implementation of the basic XMPPart behavior. As such, CPart is a treasure trove of
information about the correct way to "ignore" calls that aren't interesting because
your part handler doesn't support embedding, update asynchronously, or use offscreen
bitmaps.
Every part is embedded in another part, with the exception of theroot part, the
top-level part in each compound document. When a part is embedded in another part,
there's an object that's used to store information about the shape of the embedded part.
This boundary between a container and an embedded part is aframe -- an instance of
the class XMPFrame. Every frame has a single part displayed inside it. The container
actually embeds the frame; it knows nothing about the part inside.
Any part can be displayed in several frames at the same time. This makes it easy for a
part to be visible in several windows or to have several different presentations. For
example, a charting part might want to have one frame displaying the chart and
another allowing the data to be edited in a table.
A facet (an instance of an XMPFacet object) is a visible part of a frame. There can be many facets displaying within any given frame. This is a useful property, for instance,
when a container wants to "split" windows. Both XMPFrames and XMPFacets have a
field, partInfo, for storing information specific to the part being displayed. This is
rather like a window refCon, a handy place to store information independent of the
object itself. The CFrame and CFacet objects are designed to be plugged into the
partInfo fields of their XMPFrame and XMPFacet counterparts. The containing part
creates the XMPFrame and XMPFacet objects and then allows your part handler to
initialize their partInfo fields. In the framework, the actual work of drawing the part
on the screen is done in the CFacet object. The work of deciding what shape the
embedded part will take is done in the CFrame object. As we describe the specific
operations, we'll point out the class in which the code resides.
INITIALIZATION CODE
The first bit of code we'll consider is the initialization code for each part object. Each
distinct part in a document gets an instance of the part object, so if there are seven
little clocks running in different windows (or the same window, for that matter) there
are seven instances of the clock part object. This means that you probably want to
come up with a scheme to share any global data so that you aren't wasting space with
many copies of it. Both the Apple Shared Library Manager and SOM support
systemwide global storage, so this should be straightforward.
Resources are a special case. You'll want to be very polite about not permanently
fiddling with the resource chain or making assumptions about where your resource
file is in the chain. We suggest saving the previous head of the resource chain, setting
your file to be the end of the chain, and using the single-level resource calls (such as
Get1IndResource) to find the resources you're after. Since you'll probably want to
share the resources among separate instances of your part object, it may be better to
detach the resources you get and manage them yourself instead of counting on any
particular application heap to have the correct resource map.
THE CONSTRUCTOR
The first step in initialization is theconstructor . You should never do anything that
could possibly fail in a constructor. This pretty much limits you to operations like
setting pointer variables to NULL, setting numeric variables to appropriate values,
and making similar assignments from constants.
You can see a good example in ClockPar.cpp. The clock part simply sets up its fields
with appropriate constant values.
XMPPART::INITPART
The next phase of initialization takes place in the InitPart method that every part
object implements. The InitPart method is called by OpenDoc after the part object has
been created, and here youcan attempt things that can fail. This is where you should
attempt to allocate any extra memory you need for your part instance, get resources if
you need them, and set up your persistent storage.
Let's examine how OpenDoc's storage system looks to a part handler. When your part
object is created, the InitPart method is passed astorage unit object in which you can
persistently store information. A storage unit is really just a list of namedproperties ,
each of which has one or morevalues . Each value is an entire stream, like an existing
Macintosh file. You can do read, write, seek, insert, and delete operations on individual
values.
Each value has a type, much like the type code associated with a Macintosh file. Every
property in a storage unit can have one or more values, each with their own type code.
Thus, you can store multiple representations of any property. You can make up any
property names you like. One special property name, kXMPPropContents, is used by
OpenDoc to determine which handler goes with which part at run time. Every part
object should have a property named kXMPPropContents so that OpenDoc can determine
what part handler to run.
In our sample, CClockPart has an Initialize method, which is called by CPart::InitPart.
It sets up the menu bar for the clock and sets up a focus set for obtaining system
resources from the arbitrator (more about this later). A good example of code to set up
persistent storage can be found in the implementation of CPart. The framework calls
its own method, called CheckAndAddProperties, to make sure that the storage unit is
set up correctly.
DRAWING CODE
Now that your initialization code is in place, you'll want to make sure you can get your
part to draw onscreen. OpenDoc will call your part with the Draw method and tell you
which facet should be drawn.
Our sample, CClockPart, inherits some code from CPart that asks the CFacet object to
do the drawing. Notice, though, that before it does this, CPart::Draw sets up the
graphics port for drawing using the clipping information from the facet. This is very
similar to the basic drawing model for the Macintosh, where you draw using the
appropriate graphics port and clipping region. You can find the rest of the drawingcode
in CClockFacet::Draw. This code consists of just the straightforward QuickDraw calls
and attendant calculations needed to display either the digital or the analog clock face.
We use a utility class called CDrawInitiator to set up the drawing environment
reliably. The constructor of this class does all the work of setting the graphics port's
clipping region and origin. Later, the destructor restores the port to its previous
state. This is a tricky bit of C++ coding that takes advantage of the object allocation
behavior of stack-based objects in C++.
HANDLING LAYOUT
One of the features of CClockPart is that it presents a round shape when it's embedded.
To do this, it uses the XMPFrame object's layout negotiation features. To understand
this, you need to understand the notions of canvas, shape, and transform in OpenDoc.
• A canvas is simply a drawing context. On the Macintosh it can be either a
QuickDraw graphics port or a QuickDraw GX view port.