Writing OpenDoc Part Handlers: Hello World

From EDM2
Jump to: navigation, search

by Robert Tycast

Caveats This article presumes a basic understanding of the OpenDoc architecture, a working knowledge of C++ and experience with the SOM Developer's Toolkit.

Basic information on OpenDoc can be found in previous articles of The Developer Connection News, which can be found on your accompanying Developer Connection for OS/2 CD-ROM. The OpenDoc Technical Summary, shipped as part of the Alpha toolkit, should be required reading before attempting to build a part handler. The Class Reference Guide contains the necessary details to understand and use the OpenDoc runtime; please keep it available. These two documents also can be found on your accompanying Developer Connection for OS/2 CD-ROM.

This article describes how to build a minimal OpenDoc part on OS/2 using the Alpha toolkit provided on your accompanying Developer Connection for OS/2 CD-ROM. Since this is based on an Alpha toolkit, some of the details are going to change by the time the final product is shipped. In particular, be advised that the code in the container part sample will change. A future version of the container part will be codified to support subclassing. At that point, it might be suitable code upon which to base a production-quality part handler. The current set of sample code is definitely not warranted to be correct or complete, nor appropriate for anything other than prototyping activities designed to familiarize the developer with the OpenDoc technologies.

OpenDoc provides a set of services that enable developers to write modules that they can integrate into a document containing multiple data-types. These modules are known as part handlers. OpenDoc ensures that part handlers that manage data of various types can coexist in a document without interfering with each other.

In addition to the basic service classes of OpenDoc, an abstract class part is provided to the developer. The task, then, is to provide the implementation for the approximately 60 virtual functions that the part class defines. And of course along the way, the part handler writer must use the service objects that OpenDoc defines.

This article describes a simple part handler, "Hello World," as an example of how to subclass existing sample code in the OpenDoc toolkit. Since one must normally walk before they run (or in our case, crawl might be a more apt metaphor), Hello World implements the bare minimum of coding necessary to create a part that you can embed into an OpenDoc document.

Container Part

Even though you must implement approximately 60 methods for a part to be functional, the beginner need not concern themselves with all of them. To assist the developer, the toolkit contains sample parts. The container part class contains a default implementation for every virtual method in the part class. The hello part subclasses the container part, so the source code is shorter. This allows you to focus your attention on the methods that Hello World actively uses. Subclassing the container part gives the added benefit of inheriting the complete set of capabilities native to the container part. As a result, our simple Hello World part can embed other parts too! And the coding came free to the developer!!

Part Creation

Shortly after object creation, SOM calls the standard method, somInit. This is an ideal place to do up-front initialization that needs to be done for the part. Here, we initialize an iteration count that we will use to demonstrate event handling, as well as storing persistent data.

SOM_Scope void  SOMLINK HellosomInit(HelloPart *somSelf)
{
    HelloPartData *somThis = HelloPartGetData(somSelf);
    HelloPartMethodDebug("HelloPart","HellosomInit");
    somThis->Iteration = 0;         //Set up a value to be externalized
    HelloPart_parent_ContainerPart_somInit(somSelf);
}

Part Initialization

After you create a part, OpenDoc calls its own InitPart method:

// --- Init Part --- Entered when a part is created by "embed" menu

SOM_Scope void  SOMLINK HelloInitPart(HelloPart *somSelf, Environment *ev,
                                      ODStorageUnit* storageUnit)
{
    HelloPartData *somThis = HelloPartGetData(somSelf);
    HelloPartMethodDebug("HelloPart","HelloInitPart");
 
// Setup a Value in our storage unit to use later 

    storageUnit->AddProperty(ev,kODPropContents)->AddValue(ev,kHelloWorld Part);
    HelloPart_parent_ContainerPart_InitPart(somSelf, ev, storageUnit);
}

The method call passes the storage unit to us. It's the object that is used for all data transfer - from persistent storage (for example, the Bento file that contains the OpenDoc data), as well as more sophisticated transfers such as drag/drop, clipboard, and links. It's a handy object to have around. Later, we will save the iteration count in it. In preparation, we store a value of type kHelloWorldPart (which will hold our value for Iteration) in a property named kODPropContents.

Drawing and the Imaging Subsystem

When a part in a document becomes visible, OpenDoc calls its Draw method passing a facet and a shape. OpenDoc uses the facet to get the display frame of the parts, as well as the canvas (presentation space on OS/2) on which to draw. The shape represents the portion of the part that is to be redrawn. By intersecting it with our clip region, we draw the minimal area necessary.

// --- Draw ---

SOM_Scope void  SOMLINK HelloDraw(HelloPart *somSelf, Environment *ev,
                                  ODFacet* facet, ODShape* invalidShape)
{
   HelloPartData *somThis = HelloPartGetData(somSelf);
   HelloPartMethodDebug("HelloPart","HelloDraw");

   HelloPart_parent_ContainerPart_Draw(somSelf, ev, facet, invalidShape);

   ODFrame* displayFrame = facet->GetFrame(ev);

   // Get Presentation Space

   HPS hpsDraw = facet->GetCanvas(ev)->GetPlatformCanvas(ev);

Before we can call our platform-specific drawing routines, we have to set up our clipping region. The first step is to get our bounding rectangle to pass to the RenderPartContent method.

// Get Bounding Box

ODRgnHandle frameRgn = displayFrame->GetFrameShape(ev)->GetRegion(ev);
RECTL frameRect;
GpiQueryRegionBox(hpsDraw, frameRgn, 

Our clipping region is finally set up by calling facet::GetAggregateClipShape, which as the name implies, returns the intersection of all the clip shapes in our facet and in any facet that contains us. We set any transform in force for our facet on the clip shape. The result sets the clip region on our canvas (hpsDraw).

// Set up clipping
ODRgnHandle saveClip;
ODShape* clipShape = new ODShape;
clipShape->CopyFrom(ev, facet->GetAggregateClipShape(ev));
clipShape->Transform(ev, facet->GetContentTransform(ev));

// Don't paint over embedded parts(See footnote.)
clipShape->Subtract(ev, (ODShape*)facet->GetPartInfo(ev));

// Only paint what needs to be painted
clipShape->Intersect(ev, invalidShape);

// Put it in PM terms
ODRgnHandle clip = clipShape->GetRegion(ev);
GpiSetClipRegion(hpsDraw, clip, 

With our clip region set up, the rest of the code is traditional PM code for rendering our text, so we will not describe it here.

// Put your PM drawing routines here!
   somSelf->RenderPart(ev, facet, frameRect, hpsDraw);

There's a little housekeeping to do when we return from rendering our content: resetting the clip region on the canvas and then deleting the clip shape.

GpiSetClipRegion(hpsDraw, 0, &saveClip
facet->GetCanvas(er)->ReleasePlatformCanvas(ev);
delete clipShape;

CAUTION:

At the moment, the container part stores the embedded clips in partinfo. This is a temporary interface that is certain to change in a future version of container part.

Handling Events

// --- HandleEvent --- !!!

SOM_Scope ODBoolean  SOMLINK HelloHandleEvent(HelloPart *somSelf,
                                              Environment *ev,
                                              ODEventData*event,
                                              ODFrame* frame,
                                              ODFacet* facet)
{
   HelloPartData *somThis = HelloPartGetData(somSelf);
   HelloPartMethodDebug("HelloPart","HelloHandleEvent");

   ODBoolean handled = kODFalse;

   switch (event->msg)
   {
     case WM_BUTTON2DOWN:
           {
              somThis->Iteration++;
              frame->Invalidate(ev, kODNULL);    // Arrange for a reDraw!
           }
           handled = kODTrue;
           break;
     default:
     break;
  }
   return (HelloPart_parent_ContainerPart_HandleEvent(somSelf,ev,event,frame,facet));

} The event we handle is a right-button click. In this case, we increment the value of the iteration count. We then invalidate the frame, so OpenDoc automatically calls our Draw method. Any other events are passed back to our parent class, the container part, for handling.

Externalization

In this example, we do something very simple - we store the iteration count in our storage unit and write it out to storage. While this is simple, it illustrates the basics:

  • Getting focus on our storage unit - in this case on the kHelloWorldPart value
  • Calling StorageUnit::SetValue

We use the focus call to indicate to OpenDoc the access point in the storage unit. In other words, the focus method selects the property and position in the property where our value is located. It can be specified in combinations of Property/Value, Property/Index, Property/Offset, and many other combinations. See the OpenDoc Class Reference on your Developer Connection for OS/2 CD-ROM for more information. After the focus is set on the storage unit, the GetValue and SetValue methods on the StorageUnits work very much like accesses to a conventional file system. So you can think of a GET as a READ and a PUT as a WRITE.

// --- Externalize ---
SOM_Scope void  SOMLINK HelloExternalize(HelloPart *somSelf,
                                         Environment *ev)
{
   HelloPartData *somThis = HelloPartGetData(somSelf);
   HelloPartMethodDebug("HelloPart","HelloExternalize");

   HelloPart_parent_ContainerPart_Externalize(somSelf, ev);

// The moment of truth: write out our persistent value!

   ODStorageUnit* su = somSelf->GetStorageUnit(ev);
   su->Focus(ev, kODPropContents,kODPosUndefined,
                                kHelloWorldPart,0,kODPosUndefined);
   su->SetValue(ev, sizeof(somThis->Iteration), &somThis->Iteration);

Opening an Existing Document

We've purposely saved a method that will be called at document initialization time until last. It was important for you to understand how externalization worked before we discussed what happens when you open an existing OpenDoc document. When this happens, OpenDoc calls the InitPartFromStorage method. The part should read back from the storage unit any data that it stores in its externalize method.

SOM_Scope void  SOMLINK HelloInitPartFromStorage(HelloPart *somSelf,
                                                 Environment *ev,
                                                 ODStorageUnit* storageUnit)
{
   HelloPartData *somThis = HelloPartGetData(somSelf);
   HelloPartMethodDebug("HelloPart","HelloInitPartFromStorage");

   HelloPart_parent_ContainerPart_InitPartFromStorage(somSelf,
                                                      ev, storageUnit);
   storageUnit->Focus(ev, kODPropContents,kODPosUndefined,kHelloWorldPart,
                        0,kODPosUndefined);
   storageUnit->GetValue(ev, storageUnit->GetSize(ev),&somThis->Iteration);
}

Building Hello World

Building the Hello World part is easy. Install the OpenDoc Alpha toolkit on your system, and ensure that you can rebuild one of the sample parts. Then, create a new subdirectory called

..\OPENDOC\SAMPLES\PARTS\HELLO.

Set your default directory by typing at a command prompt:

cd ..\OPENDOC\SAMPLES\PARTS\HELLO

Copy the Hello World files from the "Source Code from The Developer Connection News" category, and type nmake.

The Hello World part will be compiled and linked. You must copy the resulting .DLL into ..\OPENDOC\BIN\HELLO.DLL, as follows:

COPY hello.dll ..\..\BIN

Running Hello World

Before you can run Hello World, you must edit ..\OPENDOC\CPPBASE\BIN\PARTS.DAT, and add the line:

HELLO PART: Hello World Sample Part

Reset your default directory thusly:

cd ..\OPENDOC\BIN and type "start DOCSHELL"

An untitled OpenDoc document will appear. Select the EMBED menu pull-down from the action bar. Select Hello World Sample Part from the menu; the part will appear in the lower left-hand corner of the document.

To move or resize it, hold down the Ctrl key and press the left-mouse button somewhere in the Hello part. A selection border and resize handles will appear. Dragging the selection border moves the part; the resize handles change its shape.

A single click in the part activates it. The activation border appears, which some describe as marching ants! Clicking with the right-mouse button increments the iteration count. Hello World now is alive and well.

As a final test, close the document by double-clicking in the upper left-hand corner of the document (with the mini-OpenDoc icon); restart it with:

"start DOCSHELL untitled"

The document should reappear with the embedded parts in the same position and shape as when it was closed, as well as the value saved in iteration count.

Since we subclassed the container part, we also have available to us the functions that are standard such as embedding, drag/drop, background color, and so forth.

Future Articles

Congratulations! You've just written your first OpenDoc part. But, hopefully not your last! Look through the Alpha toolkit for more information. There is a nice tutorial on running the demo parts in ..\OPENDOC\DOCS\DEMO.TXT. I would encourage you to work through it.

Future articles on OpenDoc will treat increasingly more complex topics. Look for titles on menu handling, clipboard, drag/drop, and linking, just to name a few.

Reprint Courtesy of International Business Machines Corporation, © International Business Machines Corporation