OD Contain Yourself
Volume Number: 12
Issue Number: 1
Column Tag: New Apple Technology
OpenDoc: Contain Yourself ...
Updating applications to work with OpenDoc
By Eric Soldan, Apple Developer Technical Support
It’s time to be an OpenDoc™ container application. It’s not too soon, and it’s not too
late. So how do you decide if your application should be an OpenDoc container
application or not? Here’s an easy test:
• Will your application benefit from Internet access?
If the answer is yes, then being an OpenDoc container application is probably the
right thing to do. By becoming an OpenDoc container application, you automatically get
the benefits of everything that is OpenDoc, including CyberDog, which will provide
component oriented Internet clients and other Internet services. Now is not the time to
ignore the Internet bandwagon. It’s worth jumping on. The only question remaining is
how to jump. Do you learn Open Transport and MacTCP and recode your entire
application to support the Internet? Or do you look for alternatives, like OpenDoc?
A logical question therefore would be: “Is OpenDoc really the fastest way to add
Internet to your application?” Adding Internet support with OpenDoc boils down to
adding basic OpenDoc support; in this article we will evaluate the difficulty of adding
OpenDoc to an existing application. Then you can decide.
The Container Application Library: Your Friend.
CALib, the Container Application Library, has been evolving for a while. It’s looking
really good right now. Joshua Susser, of OpenDoc engineering, and I have been adding
CALib support to an application I originally developed in Developer Tech Support called
DTS.Draw. I wrote DTS.Draw years ago, without ever considering the possibility of
adding something like OpenDoc later. DTS.Draw’s document model, while flexible, is a
closed model, so adding OpenDoc support is therefore a very interesting test case for
CALib.
It turns out that the amount of additional coding necessary is minimal. However,
due to CALib’s flexibility, there were many development issues that had to be discussed
for quite a while. If these discussions were not held, the implementation would not
have been as robust. I will present what we’ve learned from these discussions.
First, let’s talk about DTS.Draw for a while. When adding OpenDoc, it’s
important to understand how the existing application works. Since you are probably
not familiar with DTS.Draw, I’ll give you a quick overview.
DTS.Draw is an AppsToGo framework based application. AppsToGo is an
object-oriented C framework written by DTS. DTS.Draw is also all C code. DTS.Draw
is a “standard” drawing application. It has a tool palette of a variety of drawing
objects. It has a hierarchical document architecture which allows for grouping and
ungrouping of draw objects. It supports infinite undo. It supports drag and drop.
When a document is saved, the object hierarchy is flattened and streamed to the
file. When a drag-out occurs, the selected objects are streamed into a handle; this
stream represents a sub-hierarchy of the document. This sub-hierarchy is then just
a flavor for the clippings file. When a file is opened (or a drag-in occurs), the
hierarchy is reconstructed. Very standard stuff.
Here’s a diagram of a representative DTS.Draw document:
For a save, the objects in the hierarchy are streamed in simple tree-walk order,
as indicated by the numbering. The drag objects 2, 4, 5, and 6 can be any supported
object. The current implementation of DTS.Draw allows for line, rect, round rect,
oval, and pie objects. More can be added of course, and this exactly is how we added
OpenDoc support to DTS.Draw; we just added a new OpenDoc Part object into the
application. The difference with this object is that the content isn’t anything
specifically manipulated by DTS.Draw. It is simply a part the user adds to the
document. The DTS.Draw object just serves as a “container” for the OpenDoc part. If
a user were to add two OpenDoc parts to the above DTS.Draw document, the document
hierarchy might end up looking something like:
The implementation in DTS.Draw is relatively simple. The Part object inherits
from DTS.Draw’s native rect object. The difference is that it has a much more complex
content that the rect object in the standard DTS.Draw application. The content of the
Part object can be any OpenDoc part, of course. The content of the Part object is
managed using CALib. Given this information, look at this diagram and see exactly what
the relationship between the Part objects and CALib :
As shown above, there’s a lot more to the DTS.Draw document now. Fortunately,
most of the new stuff is managed by CALib and OpenDoc. CALib uses a “proxy part” to
contain the parts that are placed into the document content. Normally, OpenDoc expects
there to be a root part that is an OpenDoc container, but since other parts are
“embedded” into your application’s non-OpenDoc document, a proxy container part is
created and the “embedded” parts are stored in there. The proxy part, as far as
OpenDoc is concerned, is what actually contains the other parts that are referenced
from the DTS.Draw native content.
The difficulty here is that the proxy part isn’t “real.” It has no geometry
information, and OpenDoc doesn’t automatically bind the proxy part to any code, as is
the normal case with OpenDoc parts. Thus, OpenDoc is unable to offer the full range of
services for the embedded parts. But this is okay. The proxy part isn’t all that smart,
nor should it be. It is simply a part that is used by CALib to have somewhere to put the
other parts.
So it’s up to DTS.Draw to control the layout and presentation for the OpenDoc
parts. Only the application knows the geometry information for the embedded parts,
which is stored in DTS.Draw Part objects. The DTS.Draw Part object also has a
persistent reference to the corresponding object embedded in the proxy part.
Fortunately, CALib offers API calls to help manage these and other OpenDoc
interactions, so the above and other OpenDoc related tasks are relatively simple to
implement.
For example, to perform screen updates and such, DTS.Draw simply tells CALib
to tell the part to draw itself. All that is needed is the Part Ref value, which indicates
to CALib which embedded part to draw. The application doesn’t have to care what it
references. It references something, and that thing knows how to draw itself.
Streaming Your Objects
Given that the proxy part can’t really deal with containing parts by itself, (i.e.,
without your application doing the geometry), the proxy part and its embedded parts do
not constitute a valid OpenDoc document. This is important when we decide what type of
document you want to create when you save it to disk.
The choices are:
1. A Bento stream placed inside your document.
2. Your document stream placed inside a Bento document.
The right choice will of course depend on your application. For DTS.Draw, we
chose 1. Choice 1 is closer to existing Macintosh applications. Choice 2 is closer to
regular OpenDoc parts. If you were to consider the development choices, from
application to OpenDoc part, with CALib applications somewhere in between, your
choices look something like this:
Application:
Standard document architecture.
CALib-friendly application:
Standard document architecture with embedded Bento stream.
CALib-savvy application:
Bento document with application data streamed into an OpenDoc storage unit
value.
OpenDoc part editor:
Bento document with part editor data residing in an OpenDoc storage unit.
When considering drag and drop, the above categories are very important. When
applications drag something to the desktop, a clippings file is created. When OpenDoc
parts are dragged to the desktop, an OpenDoc document is created, instead of a clippings
file. This is an important distinction between applications and OpenDoc. Applications
create clippings files because they are a standard and flexible intermediate format.
OpenDoc parts create OpenDoc documents on a drag-out because an OpenDoc document is
already a universal format. There’s no reason to resort to creating a clippings file.
If you are a CALib application, the choice is no longer so clear. Our suggestion is
to base your drag-out decision on whether your document format is still “native,” or
if you use a Bento file with your native content embedded.
So, if you are a CALib application, what is one to do on a drag-out? The answer
lies in the document type. If the document type is an OpenDoc document
(CALib-savvy), then the drag-out type is OpenDoc document. If the document type is a
standard document (CALib-friendly), then the drag-out type is a clippings file.
DTS.Draw’s native content is simply streamed out. After adding CALib support,
this is still the case. The Bento document is streamed out, as well. Here’s the new
document format:
Byte offset Description
0 to 3: length of Bento stream
4 to nativeContent - 1: Bento stream
nativeContent to EOF: native content stream, i.e.,
DTS.Draw old format
There was much discussion as to whether or not the Bento stream should be at the
front of the file or the end. It turns out that you can do some interesting things if it is
at the front of the file. Try out this situation:
1. Your machine has CALib. You add some OpenDoc parts to your DTS.Draw
document.
2. You then send the document to some poor individual who doesn’t have CALib or
OpenDoc.
Guess what? If the Bento stream is in the front of the file, the unfortunate
individual can still use the file with your CALib-friendly application. The user
without CALib can actually edit the native content and save it out. The native content
changes size, but hey, it’s on the end of the file.
On machines that don’t have CALib but need to display Part objects in their
content, the standard look for “NoPart” should be used. This is what OpenDoc displays
when an editor for a particular part isn’t available. In the case of CALib applications,
it works for when CALib isn’t available, as well.
Making your CALib-based documents work on machines with or without CALib
takes a bit of extra coding, but from a version control standpoint, it is well worth it.
Documents can go anywhere. A CALib document can be given to someone without CALib.
That this person can actually edit the native content of the document, and then give the
document back to you, is pretty nifty.
On machines where CALib isn’t available, users obviously won’t be able to add
any new parts to the document. They will be restricted to editing the native content.
However, this may include moving part objects around that someone else was nice
enough to put in the document. This works. Remember that the geometry information
is stored in the native objects, which all users can edit. The proxy part doesn’t handle
the geometry. In addition, the user can delete the Part objects (which are displayed
using the “No Part” look). This of course means that there is data orphaned in the
Bento stream. (Since the user doesn’t have CALib, there is no way their machine can
modify the Bento stream.) Fortunately, the stream will be compacted the next time the
document is saved on a machine with CALib.
With some extra work, you can even allow the user to resize the Part objects
without CALib. When this occurs, the information in the Bento stream for that part is
no longer up to date. The native content object will need to be flagged as having been
resized. When the document is opened on a machine with CALib, these objects will need
to have their frame information renegotiated and resolved to reflect their new size.
We’ve been discussing types of editing that can still occur to a CALib document
Part object when CALib isn’t available. There are, of course, edits that can’t be
performed. Here are two:
1. Dragging that includes a Part object. Streaming the drag data must include
streaming the Bento information into the flavor. This can’t be done without
CALib. All that’s in the native object is a persistent reference to somewhere
inside the Bento stream. You can’t resolve the reference without CALib.
2. Copying the object. At first, it would seem that you would just copy the native
content object. It has a persistent reference. Both copies of the object would
then point to the same Bento information, i.e., the part. This is a bad thing.
When opened again on a CALib machine, each copy of the part should be
independent of the other. (You could flag the copy as an unresolved copy, and then
clone the object when the document is next opened on a CALib machine.)
As you can see from this discussion, CALib can be used to add OpenDoc capability
to your existing applications without radically changing the way they work. The actual
amount of code added to an application isn’t all that bad, either. Of course, this will
vary, depending on architecture and document content, but most likely, it’s not a
complete recode with major modifications throughout your application. Look for the
latest CALib and CASample sample code on the OpenDoc DR4 release.
Have fun out there!