TweetFollow Us on Twitter

Links in OpenDoc Parts

Volume Number: 13 (1997)
Issue Number: 6
Column Tag: develop

Supporting Links in Your OpenDoc Part

by Mike Halpin and Elizabeth Dykstra-Erickson

OpenDoc linking enables users to maintain synchronized copies of content at multiple locations, so that they can reuse content without having to manually update each time a change is made to the master copy. You may be wondering whether it's time to support linking in your part editor. This article will help you assess the difficulty and desirability of doing so -- and includes useful code samples to get you off to a quick start. In addition, it discusses linking-related requirements that apply to all parts.

Suppose a user is creating a presentation from numeric data in a spreadsheet and wants to display the same data in a variety of ways. If both the spreadsheet part editor and the presentation part editor support linking, the user can link the data and create simple to sophisticated visualizations of the data without reentering it. In addition, if the data is live (for example, if it's being generated by a stock ticker), real-time data changes can be displayed dynamically in multiple ways throughout the presentation.

Here's another scenario: The user is preparing a very large document that's shared by multiple users. If the part editors employed in the document support linking, all users can link content for which they're responsible from one portion of the document to another, keeping it synchronized at their discretion. Text, numeric data, and graphic elements can be linked to facilitate easy, immediate updates to complex documents.

Or maybe a user is creating a schematic that uses the same set of symbols in many locations. If the schematic part supports linking, the user need only create each symbol once, place it in a library, and link it to each destination location. That way, if a symbol needs to be changed, the user need only modify the source and update the links to refresh each instance of the symbol. This could be useful for symbols, shapes, or labels used on blueprints, engineering diagrams, flowcharts, organization charts, calendars -- you name it.

These examples should give you an inkling of the kind of power you put in users' hands when you support linking in your OpenDoc part. Although the user needs to plan ahead and spend the time to create the links, the link interface is easy to use and can save a lot of time and effort for data that's expected to change frequently but must be kept in sync. Linking is comparable to but much easier to implement and use than the "publish and subscribe" feature introduced in System 7.

This article will help you think realistically about what it would take to implement linking support in your part editor. The OpenDoc API aspects of linking can be pretty easily encapsulated in reusable code, either inherited from a framework such as the OpenDoc Development Framework (ODF) or lifted from the LinkingPart sample accompanying this article. The most challenging aspects of linking design revolve around content-specific issues. After giving an overview of the linking process, we'll focus on some particular issues that our experience indicates may not be obvious from the existing documentation. Even if you've already decided that your part editor isn't going to support linking, you should read the section entitled "Linking-Related Requirements for All Parts," because this still applies to you.

To benefit from reading this article, you should have at least a passing familiarity with a number of basic OpenDoc concepts. If you don't know what storage unit, persistent object, cloning, externalize, and internalize mean, we recommend the article "Getting Started With OpenDoc Storage" in develop Issue 24. This article is intended to augment information that's already available in the OpenDoc Programmer's Guide, in the OpenDoc Cookbook, and in the recipes provided with each OpenDoc Developer Release. In addition, updates are periodically posted on Apple's OpenDoc Web site at

The code examples in this article are adapted from LinkingPart, a container part that allows the arbitrary placement and resizing of any number of embedded frames within a layout grid. Selections are arbitrary collections of embedded frames, which can be cut, copied, pasted, dragged, dropped, and linked. Most of the code examples in the article have been simplified to illustrate specific points, so if you're going to adapt code from LinkingPart, you should take it from the actual sources, not from the excerpts in this article.

What's a Link, and How is it Created?

A link is a one-way path for the flow of data from a link source to a link destination. Links can be created within a single part, between two parts in the same document, or between parts in different documents. Figure 1 shows an example of data linked from a table part to a chart part in a document.

Figure 1. Linked data

To create a link, the user selects some content, copies it to the Clipboard, and then chooses the Paste As command from the Edit menu. Alternatively, the user can drag content from one location to another while holding down the Command key. In response to either action, your part editor should present the Paste As dialog box, shown in Figure 2. If both the source of the Copy or drag and the destination of the Paste As or drop support linking, the "Paste with Link" checkbox will be enabled. If the user checks this option, she can then specify either automatic or manual updating with the Get Updates radio buttons. The result will be that the pasted or dropped content will become a link destination and will be updated to reflect changes made at the source of the link.

Figure 2. The Paste As dialog box

The main work of linking consists of transmitting data between link sources and destinations. Some of this work is implemented by OpenDoc and some must be implemented by the part editor or editors involved. Later on we'll discuss the work in each category in more detail.

In addition to supporting the transfer of data from sources to destinations, OpenDoc provides a number of auxiliary mechanisms to support the user interface for creating and maintaining links. These include a notification mechanism so that parts maintaining destinations know when to acquire updated content; a navigation mechanism that enables users to locate link sources from any destination, even when the source is in a closed document; and a mechanism that protects against runaway recursive updates.

You can choose to support source links or destination links, or both (or neither). In general, viewers don't need to support link sources (although we think it would be nice for them to support link destinations!). Linking makes more sense in some part editors than in others. You'll need to use your own judgment to determine how often the user may want to create source links in your particular part editor, and balance that against the effort required of you to implement it. For example, supporting links in a chart or table part editor will be well worth the effort, while doing so in a button or sound part editor might not be.

Linking-related requirements for all Parts

Your part must comply with certain linking-related requirements whether it supports linking or not, because it might be embedded in a link source or destination. Your part must make linking-related calls whenever the user attempts to edit your part content, when your part's content changes, when the content of an embedded part changes, or when a frame is newly embedded in your part. ODF takes care of most of this for you, though you may need to override some methods to customize the behavior to your content.

When the User Attempts to Edit your Part Content

Because your part might be embedded in a link destination, your editor needs to check the link status of the part's display frame before allowing the user to make any change to the part content. The frame's link status, set for embedded frames by the containing part, indicates whether the frame is in the source of a link, in the destination of a link, or not involved in any link. Because OpenDoc links are a one-way path from source to destination, content changes shouldn't be allowed in a link destination.

When the user attempts to modify your part's content, your editor must call ODFrame::GetLinkStatus to determine the link status of your part's display frame. If the call returns kODInLinkDestination, your editor should call ODFrame::EditInLink. This will cause OpenDoc to contact the part that maintains the link destination including your display frame, so that that part can display an alert telling the user that content at a link destination can't be edited. This alert would give the user the opportunity to either find the source and edit it or break the link (as shown later in Figure 7).

If EditInLink returns false, meaning that the part containing the link couldn't be found (unlikely, but it could happen in the case of a containing part problem), your part should post an alert telling the user it isn't possible to edit this link destination. If EditInLink returns true, your part should call GetLinkStatus one more time to determine whether the link destination in which your part is embedded has now been broken by the user. If GetLinkStatus returns any result other than kODInLinkDestination, your part should allow the action initiated by the user to complete. If GetLinkStatus returns kODInLinkDestination again, you should abort that action, leaving your content unchanged.

The method TryToEdit, shown in Listing 1, can be called whenever the user has attempted to modify content. It integrates GetLinkStatus and EditInLink into a single method whose Boolean result indicates whether the attempted action should be completed (kODTrue) or aborted (kODFalse).

Listing 1. Determining whether a change should be permitted

ODBoolean LinkingPart::TryToEdit(Environment* ev, ODFrame* frame)
   ODBoolean canEdit = frame->GetLinkStatus(ev) != kODInLinkDestination;

   if (!canEdit) { 
      if (frame->EditInLink(ev))
         canEdit = frame->GetLinkStatus(ev) != kODInLinkDestination;
         this->ShowCantEditAlert(ev, frame);      // A simple alert
   return canEdit;

The ODFrame* parameter should be a display frame of your part that's embedded in the root window of the part, not necessarily the frame in which the attempted editing action was initiated. A display frame that's not embedded in the document's root window (for example, a frame associated with a view of your part in a separate window) may not be embedded in the part that's maintaining a link destination. In this case, the embedded frame may not have the correct link status.

IsFrameInRootWindow determines whether a frame is embedded in the root window of the document. Before calling TryToEdit, you should iterate your display frames until you've found one for which IsFrameInRootWindow returns kODTrue.

ODBoolean IsFrameInRootWindow(Environment* ev, ODFrame* odFrame)
   TempODWindow tWindow = odFrame->AcquireWindow(ev);
   return (tWindow != kODNULL) && tWindow->IsRootWindow(ev);

As a general rule, you shouldn't disable any particular function in response to a frame link status of kODInLinkDestination before the user has actually indicated a desire to modify content. It's better to allow the user to initiate the action and respond to the feedback that the part that actually owns the link destination will provide. For example, don't disable the Paste menu item. Instead, if the user chooses Paste from the menu, call TryToEdit, and if it returns kODFalse, do nothing. The user will already know why because a containing part displayed the alert saying that content can't be edited at a link destination, which will be more than he would know if you just disabled the menu item. Similarly, don't return kODFalse from your implementation of ODPart::DragEnter. Instead, your implementation of Drop should call TryToEdit; if TryToEdit returns kODFalse, Drop should just return kODDropFail.

When your Part's Content Changes

Because your part might be embedded in a link source, it needs to inform any containing part whenever its content changes -- for example, when the user edits content or a previous action is undone or redone. It does this by calling ODFrame::ContentUpdated, shown in Listing 2. Calling ContentUpdated will cause each link source that embeds your part, either directly or indirectly, to write its content including your part to an ODLinkSource object (explained later under "OpenDoc Linking Classes"). If your part is embedded in a link source with automatic updating, this will cause your CloneInto method to be called immediately and your part to write out all of its content. Other data contained or embedded in the same link source will also be written out, even if that data hasn't changed. Finally, one or more link destinations may be updated.

Listing 2. Making content changes known

void LinkingPart::ContentUpdated(Environment* ev, ODUpdateID updateID)
   // Iterate over our display frames.
   CListIterator iter (fDisplayFrames);
   for (CDisplayFrameProxy* proxy = (CDisplayFrameProxy*) iter.First();
         proxy = (CDisplayFrameProxy*) iter.Next()) {
      // If the display frame is real (has been "connected" or was 
      // "added"), call ContentUpdated for the frame; otherwise, 
      // ignore it. 
      if (proxy->FrameIsLoaded(ev))
         proxy->GetFrame(ev)->ContentUpdated(ev, updateID);

In general, even with relatively small amounts of content involved in the update, this process takes a noticeable amount of time. For this reason, you don't want to make this call on each atomic content change -- for example, every time a character is typed. Instead, a good rule of thumb is that ContentUpdated should be called for at least the same granularity of change for which you create an individual undoable, redoable action state. When the user begins some activity that would create a new undoable action separate from the previous changes, you should probably call ContentUpdated if any changes have been made since the last time it was called.

A corollary to this is that you should call ContentUpdated following the undoing or redoing of any action states you've created. A less obvious corollary is that you should call this method before losing the selection focus if any changes have been made since the last time it was called. Once another part has the focus, it may begin adding new undoable actions, and it's important to preserving the order of undoable operations that any linking-related consequences of the changes in your part be completed first. You may also choose to execute a deferred call after some preset or user-settable amount of idle time. You'll need to determine this empirically by user testing with your particular content (make sure that it's embedded in a link source and you can see the destination updating -- otherwise you won't learn much from this!).

The bottom line is that if the user has embedded your part in a link source, you shouldn't make updates so infrequent that the user thinks the link isn't working, nor so frequent that it's difficult for the user to complete a simple task. If the user finds that link updates are slowing performance too much, she can choose manual instead of automatic updating of the containing link source and control the process herself. The embedded part doesn't know when this happens, so it continues to call ContentUpdated as often, but the call will now take an infinitesimal amount of time to execute.

When Frames are Embedded in your Part

Even if it doesn't support linking, your part must set the link status of every frame embedded in it. After a newly embedded frame has had its containing frame set to one of your part's display frames by calling ODFrame::SetContainingFrame, your part should call ODFrame::ChangeLinkStatus and pass in the appropriate link status. If your part doesn't support linking, it should pass in kODNotInLink.

Any change in the link status of any of your display frames (which can only be made by a containing part) causes OpenDoc to call your part's implementation of ODPart::LinkStatusChanged. If your part supports embedding, your implementation of LinkStatusChanged should call ODFrame::ChangeLinkStatus on each frame that's embedded in the frame specified in the ODFrame parameter to LinkStatusChanged.

You can always pass kODNotInLink to ChangeLinkStatus, and OpenDoc will automatically set the embedded frame's link status to that of its containing frame. The code in Listing 3 will work for a part that doesn't support linking and that maintains a list of embedded ODFrame objects. Implementing this ensures that parts embedded in your part will obtain accurate information regarding their link status in their implementation of TryToEdit.

Parts that do support linking must filter out frames that are involved in their own links. Their status within a local link source or destination takes precedence over the link status of the containing frame hierarchy.

Listing 3. LinkStatusChanged implementation when you don't support linking

void LinkingPart::LinkStatusChanged(Environment *ev, ODFrame* odFrame)
   COrdListIterator iter(fEmbeddedFrames); 
   for (ODFrame* odEmbeddedFrame = iter.First(ev); 
         iter.IsNotComplete(ev); odEmbeddedFrame = iter.Next(ev)); {
      TempODFrame containingFrame = 
      if odFrame->IsEqualTo(ev, containingFrame)
         odEmbeddedFrame->ChangeLinkStatus(ev, kODNotInLink);

Human Interface Elements

Several human interface elements are involved in synchronizing content within a single document and across multiple documents. We'll briefly describe these elements here without a detailed discussion of the OpenDoc API calls that invoke them, since those calls are covered in the OpenDoc Programmer's Guide. Details of the appearance and behavior of these interface elements are provided in the OpenDoc Human Interface Guidelines (Part 3 of the Programmer's Guide).

The Paste As dialog

Though it also serves functions related to embedding and translation, the Paste As dialog (illustrated earlier in Figure 2) is essential to the creation of links. As mentioned previously, this dialog is invoked when the user chooses Paste As from the Edit menu or drags and drops with the Command key held down (and the drop target document in front). It's displayed by calling ODClipboard::ShowPasteAsDialog or ODDragAndDrop::ShowPasteAsDialog.

Link borders

Linking requires that the user be able to distinguish links from other content. OpenDoc employs link borders to show the user where linked data exists within a document. The user can display the borders of all linked content by checking the Show Links checkbox in the Document Info dialog box. When the user selects content that contains a link or sets an insertion point inside a link, your part editor should display the link border even if the Show Links checkbox is off. Clicking a link border selects the link, which is indicated by a change in the appearance of the link border.

The usage and appearance of link borders are described in the Human Interface Guidelines. The current guidelines specify a border width of four pixels (two pixels created via a fill pattern, with one additional opaque white pixel on either side). Because we've found the four-pixel width is too small a target, we recommend a width of five pixels (three pixels with the fill pattern and one transparent pixel on either side, as shown in Figure 3). The additional pixel also makes the difference between the selected and unselected appearance clearer. Because the precise form of the link border and the determination of what's selected depend on the content kind, parts must implement the drawing of link borders as well as determine when they should be shown around any given content.

Figure 3. Link border patterns

The Link Info dialogs

When the user selects a link in your part, your part editor should enable the Link Info command in the Edit menu. If this command is invoked, your editor needs to display the appropriate dialog box (as shown in Figure 4) -- the Link Source Info dialog box if the current selection is a link source, or the Link Destination Info dialog box if the selection is a link destination. The Link Source Info dialog allows the user to initiate the update of a manual link or break the link so that the existing content behaves as ordinary unlinked content. The Link Destination Info dialog offers the same two choices and also allows the user to navigate to the source of the link. These dialogs are displayed via the calls ODLinkSource::ShowLinkSourceInfo and ODLink::ShowLinkDestinationInfo.

Figure 4. The Link Source Info and Link Destination Info dialog boxes

In addition, these dialogs allow the user to specify when to send updates from the link source and when to receive updates in the link destination. Note that update settings at the source and the destination are independent. Immediate, automatic updating from the source to the destination can occur only within documents; updating can happen automatically across documents only upon saving the source document. Further, automatic updating from the source to the destination link can occur only if "On Save" is selected in the Link Source Info dialog (the default when a link source is first created) and "Automatically" (the default in the Paste As dialog) is selected in the Link Destination Info dialog.

"On Save" refers to the fact that changes made will become available to destinations in other documents when the source document is saved, without additional (that is, manual) user intervention. From the implementation perspective it should be thought of as automatic. OpenDoc's implementation hides the fact that an update to a link source that the part has posted automatically won't propagate to a destination in another document until the source document is saved.

If automatic updating is specified for a source that your part creates, you should automatically provide updated content to the link when the source content changes (or at periodic intervals); otherwise, your part should provide updates only when the user clicks the Update Now button in the Link Source Info dialog. If automatic updating of a destination is specified, it should occur whenever the part containing the destination is notified via the LinkUpdated API call. (For destinations in the same document as the source, this will occur immediately after the update is provided at the source. When the source is in another document, it will occur after the source document is saved following the source update.) Manual updating should occur when the user clicks the Update Now button in the Link Destination Info dialog.

Parts maintaining automatic link destinations must register with ODLink objects to receive automatic notification when the ODLink has been updated, by calling ODLink::RegisterDependent.

In response to a user's clicking Find Source in the Link Destination Info dialog, your editor should call ODLink::ShowSourceContent. OpenDoc responds by calling ODPart::RevealLink on the part containing the link source, first opening the source document if necessary. The part maintaining the link source should then select the source content and scroll it into view.


We mentioned earlier that OpenDoc provides a mechanism that protects against runaway recursive updates. If updates are automated and the user creates a link destination within its own source or a chain of linked content that feeds updates back to the source, this could lead to an endless recursion of updates (until an update eventually failed due to insufficient memory). However, OpenDoc detects this situation and presents the link-cycle alert (shown in Figure 5) on the second and subsequent recursions, allowing the user to interrupt the cycle or continue updating for one more cycle. There's no part involvement in this process, other than responding to each link update according to the normal recipes.

Figure 5. The link-cycle alert

In most cases where a link destination is contained within its own source, the user will want to interrupt the updating because more than one update cycle would be pointless. So when would the user want to continue updating for one more cycle? Permit us to digress for a moment from the topic of alerts while we describe the situation where this would happen.

Suppose a link source and its link destination are both contained within a second link source. Copies of both link source 1 and link destination 1 will then be contained in link destination 2, as illustrated in Figure 6. Modifying the content of link source 1 will cause two separate updates to link source 2, one due to the original content change and the second due to the updating of link destination 1. In this case, OpenDoc will post the link-cycle alert before completing the second update, because it has no way to tell whether a second update is justified. If the change in the content of link source 1 is propagated via link source 2 to link destination 2 before it's propagated via link source 1 to link destination 1, the copy of link destination 1 contained in link destination 2 won't reflect the final content of link destination 1 until the user allows a second update to be completed by clicking the Update button in the link-cycle alert.

Figure 6. A situation where two update cycles may be required

Besides the link-cycle alert, which is displayed automatically by OpenDoc, there are three other alerts that your part editor might need to display in different linking-related situations (the resources and code to implement these alerts are available in the LinkingPart sample as well as in ODF):

  • If the user attempts to edit a selection in a link destination in your part, or if your part's EditInLinkAttempted method is called (indicating an attempted change to the content of an embedded frame that's contained in a link destination in your part), you should display the alert shown in Figure 7. The Break Link and Find Source buttons do the same thing as the corresponding buttons in the Link Destination Info dialog.
  • If the user attempts to edit a selection that involves the content of more than one link destination in your part, you should display the alert shown in Figure 8.
  • If your part is embedded in a link destination, and a call to ODFrame::EditInLink returns false, you should display the alert shown in Figure 9. If EditInLink returns true, the part that actually maintains the link in which your part is embedded has displayed the alert in Figure 7 in its implementation of EditInLinkAttempted.

Figure 7. A situation where two update cycles may be required

Figure 8. A situation where two update cycles may be required

Figure 9. A situation where two update cycles may be required

Basic Mechanisms of Opendoc Linking

Now we'll describe the basic mechanisms that make linking work in OpenDoc. Our purpose is to give you a conceptual understanding that will allow you to think about how linking can be implemented in your part. We won't mention every OpenDoc API call that must be made during the process; for those details, you'll need to consult the OpenDoc Programmer's Guide and the recipes. Most of these mechanisms can be provided by generic, reusable code. The easiest way to get all of it right is to base your part on ODF. You can also examine the implementation in the LinkingPart sample and lift code from there.

OpenDoc Linking classes

The fundamental agents in the process of synchronizing link destination content with link source content are pairs of persistent objects, from the OpenDoc classes ODLinkSource and ODLink. Like other persistent objects, they're represented by storage units in the document draft and by SOM objects in memory when they've been internalized by a part that uses them. During the process of creating a link, the part editors involved ask OpenDoc to create one object of class ODLinkSource and another of class ODLink. The ODLinkSource object provides a storage unit to which the part that owns the source content writes updates as that source content changes. The ODLink object provides a storage unit from which the part that owns a link destination can read the updated versions of the source content that have been written out to the ODLinkSource object.

Within any document, there's always a one-to-one relationship between ODLinkSource and ODLink objects. An ODLinkSource object is also always associated with exactly one ODPart instance, and by that part with one specific content subset within it. In contrast to this, within a single part or scattered among multiple parts there can be any number of link destinations, each of which obtains content updates from the same ODLink object, and each of which is responsible for maintaining and updating a separate copy of the source content.

Writing content to an ODLinkSource object and reading it from an ODLink object are very much like performing the same operations with the ODClipboard and ODDragAndDrop objects. Since parts are required to support these operations, this key portion of linking support should require little additional code in your part editor. However, unlike ODClipboard and ODDragAndDrop objects, there can be any number of paired ODLink and ODLinkSource objects in existence, and these objects and the data within them persist when a document is saved, closed, and reopened.

Besides ODLinkSource and ODLink, five other classes relate to OpenDoc linking:

  • ODLinkManager isn't used by part editors, but it's accessed explicitly by container applications (traditional applications that support embedding of OpenDoc parts).
  • ODLinkSpec is a nonpersistent class used as a token for initially establishing a link source-link destination connection.
  • ODLinkInfo is a simple structure maintained by part editors for each distinct link destination. It records information needed when displaying the Link Destination Info dialog, and also certain characteristics of the particular destination, such as whether it's to be updated manually or automatically.
  • ODPasteAsResult and ODLinkInfoResult are simple structures that provide access to user feedback from the Paste As and Link Info dialogs.

How a link is established

Let's look at how a link is established between parts within a document. The source part (the part that places data on the Clipboard or in a drag and drop object) and the destination part (the part receiving the paste or drop) each follow a different procedure.

A part that's copying content to the Clipboard or a drag and drop storage unit advertises its ability to create a link by writing a link specification in addition to content. It does this by calling ODDraft::CreateLinkSpec, passing in a reference to the ODPart object as well as an ODByteArray argument containing a private token that will later allow it to identify the subset of content from which a link is to be created. The token can be anything the part finds useful, such as a pointer to an object in the part representing the potential link source and its content. The part then adds the kODPropLinkSpec property to the data transfer storage unit and calls ODLinkSpec::Write, which writes the link specification out to the property.

The method shown in Listing 4, simplified from LinkingPart, takes advantage of CLinkSource being a reference-counted class. It isn't required that the class representing a link source in your content be reference-counted (ODLinks and ODLinkSources are reference-counted), but it helps. In our method it avoids the need for additional code to determine whether the linkSrc, which may or may not be a newly created object with no other references, needs to be deleted in the event that creating or writing the link specification fails. The reference-counted CLinkSource reference returned by this method will be stored by the caller in an appropriate location.

Listing 4. Writing a link specification (source part)

CLinkSource* LinkingPart::CreateLinkSpec(Environment* ev, 
                                          ODStorageUnit* su);
      // Determine whether the selection can be published.
   if (!fSelection->CanPublish())
      return kODNULL;

      // If the selection being copied is exactly a link source, we should
      // use that, not create another.
   CLinkSource* linkSrc = fSelection->FindLinkSource();
   if (linkSrc == kODNULL)
      // Make a new link source based on the current selection.
      linkSrc = new CLinkSource(fSelection, ...);
   ODLinkSpec* linkSpec = kODNULL; 

   // Make the data a pointer to our link source object.
   TempODByteArray data = CreateByteArray(&linkSrc, sizeof(linkSrc));

   linkSpec = this->GetDraft(ev)->CreateLinkSpec(ev, this->GetODPart(), 
   su->AddProperty(ev, kODPropLinkSpec);
   linkSpec->WriteLinkSpec(ev, su);
   linkSrc = kODNULL;

   // Failing to write a link spec isn't fatal. The user just won't be
   // able to create a link, so don't RERAISE. 
   if (linkSpec)
      delete linkSpec; 
   return linkSrc;

Reference counting in LinkingPart is inherited from the mixin class MRefCounted. Its constructor initializes an fRefCount member to 1. The Acquire method increments this. The Release method decrements it, and if decremented to 0, invokes the object's destructor by calling delete this. Objects that have "acquired" a reference to a reference-counted object call its Release method and clear their reference, rather than explicitly deleting it when it's no longer needed in that context. This eliminates the need to explicitly decide which of two or more objects that maintain references to a single other object is responsible for deleting it.

CanPublish determines whether it's appropriate to allow creating a link source from the selected content. The editor must enforce two rules:

  • A link source shouldn't be created from a selection that includes part of a link destination, because when the destination updates there won't be any way to determine what portion of the updated content will affect such a link source.
  • A link source shouldn't be created when its content is exactly the content of a link destination; in this case the user should create additional destinations directly from the original source rather than creating a chain of links.

Parts may impose additional restrictions. FindLinkSource determines whether the current selection is exactly the content of an existing link source. If so, linkSpec will reference the existing CLinkSource object. The implementations of both CanPublish and FindLinkSource are too content specific to be worth showing here.

When the user chooses Paste As from the Edit menu (the menu command is kODCommandPasteAs) or drags while holding down the Command key (the kODDropIsPasteAs bit is set in the drag attributes), the part in which the user wants to create a link destination calls the ODClipboard::ShowPasteAsDialog or ODDragAndDrop::ShowPasteAsDialog method, passing kODTrue as the canPasteLink parameter. If a link specification property is present in the data transfer storage unit, the "Paste with Link" checkbox is enabled in the Paste As dialog box. The user's choice is indicated in the pasteLinkSetting field of the ODPasteAsResult structure.

When the user has chosen to create a link, the drop or paste target obtains an ODLinkSpec object by calling ODDraft::CreateLinkSpec, this time passing kODNULL arguments for the ODPart and the ODByteArray data. It then focuses the data transfer storage unit on the kODPropLinkSpec property and calls ODLinkSpec::Read. This produces an ODLinkSpec object identical in content to the one that was created at the source of the copy.

The destination part then passes the ODLinkSpec to ODDraft::AcquireLink. OpenDoc obtains an ODLinkSource object by calling ODPart::CreateLink on the source part (the link specification was initialized with this reference), passing the ODByteArray contained in the link specification as an argument. The source part's implementation of CreateLink (Listing 5) identifies the content that needs to be linked from the ODByteArray data. If the specific set of source content isn't yet associated with an ODLinkSource, it calls ODDraft::CreateLinkSource to obtain a new ODLinkSource object; otherwise, it returns the one it already has. At this time, the source part also updates the content of the ODLinkSource object. For an existing link, this ensures that each representation of the linked content that was available on the Clipboard or in a drag and drop object will also be available to the new link destination.

CLinkSource::CreateLink, which is called in Listing 5 for a newly established link, calls ODDraft::CreateLinkSource, and also its own ContentUpdated method, and takes care of other details that ensure that the newly linked content behaves as it should.

Listing 5. Creating the link (source part)

ODLinkSource* LinkingPart::CreateLink(Environment* ev,
           ODByteArray* data)
   CLinkSource* pendingSource = *((CLinkSource**)data->_buffer);
    ODLinkSource* odLinkSource = pendingSource->GetODLinkSource();
   if (odLinkSource == kODNULL) {   // New link source
      odLinkSource = pendingSource->CreateLink(ev);

      // To support asynchronous calls to this method resulting from a
      // cross-document Command-drag link creation, InitiateDrag posted
      // a CCreateLinkSourceAction to the undo history "just in case"
      // and stored a reference in the pending CLinkSource.
      CCreateLinkSourceAction* linkAction = 
      if (linkAction == kODNULL) {   // Paste As from the Clipboard
         linkAction = new CCreateLinkSourceAction(this);
      // The action won't ever undo or redo anything unless it has a
      // link source reference.
      // CLinkSource::ContentUpdated actually writes or promises the
        // current data to the link source.
      pendingSource->ContentUpdated(ev, kODUnknownUpdate, kODTrue);
    return odLinkSource;

An ODLink object associated with the acquired ODLinkSource is returned as the result of the destination part's call to ODDraft::AcquireLink. The destination part acquires the actual linked content by reading content from the ODLink object.

Creating links between documents is somewhat more complex for OpenDoc, though this is largely transparent to part implementations. It involves the help of the ODLinkManager objects in each session and the creation of an edition file. In this case, calling the CreateLink method of the source part and the ensuing update occur asynchronously after the drop or paste is completed. Though this affects the way undo transactions are posted, the existing recipes already take this into account. The call to pendingSource->GetPendingAction in Listing 5 is related to this issue.

In addition to establishing new links, Clipboard and drag and drop operations can move or copy existing links with their content. The OpenDoc Human Interface Guidelines specify when link sources and destinations contained in content that is being transferred should be established where dropped or pasted. Part editors determine whether a link source or destination can be written to a data transfer object, and whether a link source or destination can be established when read in from a data transfer object, by validating storage unit references and cloned object IDs as indicated in the linking recipes. In addition to writing out references to ODLink and ODLinkSource objects as described there, part editors must write out additional content model-specific data for each link that allows its relationship to a specific subset of content to be reestablished when it's read back in.

How and when links are updated

Part implementations have complete control over the updating process. You can think of the ODLinkSource and ODLink objects as the ends of an open tube, sloping toward the ODLink end. Source parts place updated content into the ODLinkSource end by obtaining access to the ODLinkSource's content storage unit and writing the source content to it, then calling ODLinkSource::ContentUpdated. Destination parts retrieve content from the ODLink end by obtaining access to its content storage unit and reading the new content from it.

Multiple representations of link source content can be promised when updating. The ODLinkSource object will immediately fulfill promises for those representations that are in use at one or more link destinations. Any remaining unfulfilled promises will never need to be fulfilled. (Promises are defined and discussed on pages 330-332 of the OpenDoc Programmer's Guide.)

The decision about when to write content to the ODLinkSource object and when to read it from the ODLink object depends on whether link updating is designated as automatic or manual. Manually updated link sources in your part don't put data into the ODLinkSource object, and manually updated destinations don't retrieve data from the ODLink object, until the user requests an update for a particular link in one of the Link Info dialogs.

With automatic updating, a link source writes modified content to the ODLinkSource object without explicit user intervention. The decision about when to do this is content specific but should follow the guidelines discussed earlier in this article under "When Your Part's Content Changes." A part that contains one or more automatically updated link destinations for a given ODLink object will replace the existing content of each one by reading new content from the ODLink object when it's notified that the link content has changed via a call to its LinkUpdated method.

Update IDs

Each block of part content at the source or the destination of a link must have an associated update ID, which is fundamental to the updating process. An update ID is created when the source part calls ODSession::UniqueUpdateID. This ID makes it possible to do the following:

  • protect against runaway recursive updating
  • control the enabling and disabling of the Update Now button in the Link Info dialogs
  • determine whether an immediate update notification is required when ODLink::RegisterDependent is called

A new update ID is obtained where content has been modified, even if the part editor doesn't support linking. It's passed to ODFrame::ContentUpdated, which propagates it to all containing parts via their EmbeddedFrameUpdated methods. It becomes associated with the current content of a paired ODLinkSource and ODLink when it's passed to ODLinkSource::ContentUpdated. It's retrieved by link destinations as they update.

When a single ODLinkSource object has its ContentUpdated method called twice consecutively with the same update ID, it will display the link-cycle alert to allow the user to continue or abort the update at this point. For this mechanism to work, the update ID obtained when updating an automatically updated link destination, or from EmbeddedFrameUpdated, must be passed on to any automatically updated link source that contains the modified content. This isn't necessary for manually updated links, since you can't have a runaway update cycle when a manually updated link is involved; to suppress the mechanism in this case, a new update ID should be substituted before calling ODLinkSource::ContentUpdated or ODFrame::ContentUpdated (the latter applies only when the destination is manually updated).

Enabling of the Update Now button in the Link Info dialogs and triggering of an immediate notification in ODLink::RegisterDependent are simply controlled by comparison of the update ID passed in (as a separate parameter to RegisterDependent and ShowLinkSourceInfo and as a member of an ODLinkInfo record in ShowLinkInfo) with the update ID stored in the ODLink or ODLinkSource object when ODLinkSource::ContentUpdated was last called.

The ODClipboard object also associates a unique update ID with the current Clipboard contents. This in turn is used by a linking part to determine whether it should remove from the Clipboard a link specification that's no longer viable (for example, if the content that was to comprise the link source is removed, or an existing link source is broken). This usage is independent from the usage of ODUpdateIDs in the link updating process.

Embedded frames and updating

ODPart::EmbeddedFrameUpdated must be implemented by container parts that support linking. The implementation should identify any link sources in which the specified frame is embedded. Each such automatically updated link source should be updated at this time. A manually updated link source will need to be tagged (typically with the ODUpdateID passed to EmbeddedFrameUpdated) so that the Update Now button will be enabled the next time the Link Source Info dialog is invoked by the user.

When the content of an embedded part changes, the part notifies each of its display frames. The containing part may consequently receive multiple calls to EmbeddedFrameUpdated, each specifying a different embedded frame. To prevent unnecessary data transfers and unnecessary display of the link-cycle alert by OpenDoc, the implementation of EmbeddedFrameUpdated must perform some filtering before propagating the update to any containing link sources.

In LinkingPart, as in many container parts, each embedded frame is represented in the part by a frame proxy object, and all the embedded frame proxies associated with a single embedded part are grouped in a single proxy object in the part's content model. When the proxy containing the frame specified in EmbeddedFrameUpdated is identified, it's responsible for updating any affected link sources. Before doing so, it must determine whether the update is merely a repeat of a previous one via an additional embedded frame. If so, it suppresses updating the link source again with the same ID.

This isn't just a matter of storing the update ID each time EmbeddedFrameUpdated is called and ignoring subsequent updates with the same ID. As discussed earlier, there are legitimate situations where the link should be updated twice with the same ID so that OpenDoc will display the link-cycle alert. The extra EmbeddedFrameUpdated calls that should be ignored not only duplicate the update ID of a previous call that involved the same proxy but are also associated with an embedded frame that hasn't previously presented an update using the same ID.

CContentObj::EmbeddedFrameUpdated is called on each shape in the part content from LinkingPart's override of ODPart::EmbeddedFrameUpdated, until one of them returns true, indicating that the "shape" containing the specified frame has been notified. This method, which returns kODFalse for any nonembedding shapes, is overridden in CEmbeddingShape as shown in Listing 6.

Listing 6. Overriding CContentObj::EmbeddedFrameUpdated

ODBoolean CEmbeddingShape::EmbeddedFrameUpdated(Environment* ev, 
      ODFrame* frame, ODUpdateID newID)
   CEmbeddedFrameProxy* foundProxy = this->GetProxyForFrame(ev, frame);
   if (foundProxy == NULL)
      return kODFalse;        // Frame not in this shape -- keep looking.

   ODBoolean shouldUpdateLinks = kODFalse;
   if (newID == fLastUpdate) {   // We're getting a possibly
                               // redundant update.
      if (foundProxy->GetLastUpdate() == newID) {
         // A second update with the current ID from the same frame 
         // means that this is a legitimate update. 
         shouldUpdateLinks = kODTrue;
         // We need to restart the algorithm by clearing each proxy. The
         // state leaving here is equivalent to the state leaving the
         // else clause below.
         COrdListIterator iter(fFrameProxies);
         for (CEmbeddedFrameProxy* frameProxy = 
               (CEmbeddedFrameProxy*)iter.First(); iter.IsNotComplete(); 
               frameProxy = (CEmbeddedFrameProxy*)iter.Next()) {
   else {   // We haven't seen this update ID before, so propagate the 
            // update.
      fLastUpdate = newID;
      shouldUpdateLinks = kODTrue;
   // Passing kODFalse to ObjectUpdated allows each containing link 
   // source to be updated regardless of its previous update ID. This 
   // allows OpenDoc to detect an unnecessary cyclical update and
   // respond appropriately.
   if (shouldUpdateLinks)
      this->ObjectUpdated(ev, change, kODFalse);
   return kODTrue;   // Found the shape with the frame, so stop looking.

Linking In Your Part

The previous section, in describing the basic mechanisms of OpenDoc linking, touched on some of the responsibilities of part implementations. The real work in supporting linking is in managing the relationships of the ODLink and ODLinkSource objects with specific content subsets, and particularly in managing the replacement of content when link destinations are updated. These aspects are inherently content specific; however, we have found that certain basic structures are useful across a broad range of content models and have based the discussion and examples on these.

Classes relating to specific content

We've found that some classes are generally useful for parts to define regardless of their content kind. A class or hierarchy of classes representing the part's content -- CLinkingPartContent, in LinkingPart -- can be used to represent the entire part's content, the selection, promised data, contents of link sources and destinations, and the data stored in undo actions. It's possible to hide a lot of content-specific behavior in this class. By manipulating content class objects, code that implements Edit menu choices, dragging, dropping, and managing links can be more or less generic. ODF provides an abstract content class that you must override to represent your part's specific content.

A fundamental behavior that characterizes the content class is the ability to write the content to an OpenDoc storage unit, and to initialize itself by reading from a storage unit. Since OpenDoc requires that the representation of content be identical when externalized to the part storage, or cloned to the drag and drop object, the Clipboard, or an ODLinkSource-ODLink pair, it's convenient to use the same class to implement each of these behaviors.

Link sources and destinations can each be represented by a class that stores a reference to an ODLinkSource or ODLink object, along with essential state information for interacting with those objects and a reference to a content object. For a link source, the content object represents the subset of the part content that will be written whenever the ODLinkSource object is updated. For a link destination, it represents the content that was read from the ODLink object during the most recent update. The class declarations in Listing 7, simplified from those in LinkingPart, represent the more generic data members of these classes.

Listing 7. CLinkSource and CLinkDestination class declarations

class CLinkSource 
   void ContentUpdated(Environment* ev, ODUpdateID updateID);
   ODLinkSource*   fODLinkSource;
   ODID                fODID;  // Used to obtain fODLinkSource after cloning
                                // its storage unit
   ODUpdateID      fUpdateID;  // For ODLinkSource::ContentUpdated and
                                // ShowLinkSourceInfo
   ODUpdateID      fPendingID; // Determines when to remove a link spec from
                                // the Clipboard
   ODPart*         fODPart;    // For ODLinkSource::SetSourcePart
   CCreateLinkSourceAction* fPendingAction; // Action data created during drag
                                             // initiation
   CLinkingPartContent  fContent;

class CLinkDestination
   void LinkUpdated(Environment* ev, ODUpdateID updateID);
   ODLink*                   fODLink;
   ODID                        fODID;
   ODLinkInfo              fLinkInfo;      // Contains updateID, passed to
                                            // ShowLinkDestinationInfo
   ODPasteAsResult*       fPasteAsResult;  // Choices for embed/merge, editor,
                                            // translation etc.
   ODBoolean               fRegistered;    // Is this registered for automatic
                                            // updating?
   ODPart*                 fODPart;        // For ODLink::RegisterDependent
   CLinkEndAction*     fEndAction;         // For ending paste link
                                           // transaction asynchronously
   CLinkingPartContent*   fContent;

Despite the obvious symmetry between these classes, there are fundamental differences between link sources and destinations. Link sources can overlap or be nested, can contain entire link destinations, and don't affect the operations that can be performed on their content. A given portion of content can be involved in a number of distinct link sources. An existing content selection becomes the content of a link source when the user creates a link based on that selection.

In contrast, link destinations can't overlap or contain other link destinations or link sources, and place strict limitations on the operations that can be performed on their content. A given portion of content can be involved in only one link destination. Existing nonlinked content never becomes the content of a link destination. When it receives its initial update, the link destination is always populated by a copy of the content that comprises the associated link source.

Figure 10. How the part objects work with the OpenDoc objects.

Figure 10 shows how the CLinkSource and CLinkDestination objects work with the OpenDoc objects described earlier. The CLinkSource object maintains the relationship between a portion of content (CPartContent) and an ODLinkSource object. To update the ODLinkSource, it obtains from it a reference to an ODStorageUnit and passes that to the CPartContent object, which in turn writes itself to the storage unit. The CLinkDestination object maintains the relationship between an ODLink object and the portion of content that's to be replaced when the link destination updates. To update the destination, it obtains a reference to the ODStorageUnit from the ODLink object and passes that reference to an empty CPartContent object, which in turn initializes its content by reading from the storage unit. Finally, it performs the necessary operations to disengage its old content from the part content and engage the new content.

Updating link destination content

Now we'll discuss some issues you'll need to address if your part supports link destinations.

When a link destination is updated, there's no way that the new data can be apportioned to update more than one distinct data object; all of the old link destination content must be replaced with all of the update content. A single data object can be updated in place, or multiple objects can be removed and replaced all at once. (The latter can be messy, since references to the replaced objects may need to be removed and replaced in numerous locations: the part content, the selection, one or more containing link sources, and any undo action data, such as a paste action in which a link destination was pasted.) In either case, this implies that any object (for example, link sources or undo action data) whose relationship to link destination content must be preserved across an update must refer to all of the link's content. This in turn implies that in general, changes can't be made to just a portion of link destination content.

A portion of a link destination can be selected and copied to the Clipboard or dragged and dropped if the result of doing so is a copy rather than a move. A link source can't be created from such a selection. Such a selection itself also can't remain selected across a link update because there's a way to decide which portion of the new content should be selected. Parts can choose to allow some non-undoable local modifications to a portion of a link destination's content. These, like the selection, will be blown away by the next link update.

In supporting the multiple undo/redo capability required of all OpenDoc parts, an important simplifying assumption is that an action state is responsible for transitioning back and forth between two fixed states of the part's content. While the content changes that drive the link updating mechanism are undoable editing actions, the link updates themselves are never "undone." When the user undoes or redoes a change to link source content, the ODLinkSource object and ultimately the link destination content are simply updated with the current version of the content. There's no mechanism in OpenDoc that allows an updating destination to recognize a reversal and simply restore the content from undo data rather than reading it in from the ODLink object again.

For some content models, this may be a moot point. For example, for simple text content there's really no meaningful distinction, since the resulting text would be the same whether it was transmitted through the link or restored from action data stored locally at the destination. Text characters are unique system resources. The "objects" themselves, referred to in the text stream by ASCII or Unicode tokens, are the same, no matter where identical token streams come from.

For other content models, specifically those that include embedded content, this is an issue that bears some analysis. The LinkingPart sample represents each link destination and its included content as a single object within its content model. Link updates do replace multiple individual objects (this is inevitable when the link content can consist of multiple embedded frames), but this is hidden from the rest as an update to a single object, which itself persists. This neatly sidesteps the undo question. From outside the CLinkDestination object, undo actions are still operating on fixed undone and redone states since the objects visible to them remain constant. The data that underlies the updated link destination may consist of different objects, though these objects are equivalent in that they're new copies of the same content at the link source.

Likewise, the effect of link destination updates on containing link sources is invisible to the CLinkSource object. Of course, its ContentUpdated method will be called to propagate the changes, but the representation of the link source content in the CLinkSource object will remain constant, because only the same CLinkDestination object is referred to, never the content that's replaced during an update.

There ain't no such thing as a free lunch. The price paid for simplifying link destination updates is that the CLinkDestination object has to serve as a proxy for its actual content, providing pretty much the entire interface for nonlinked embedded parts and any intrinsic content. The good news is that in LinkingPart, operations on all content are always performed via a containing CLinkingPartContent object that iterates over the actual elements (link destinations and embedded parts). For many cases, CLinkDestination merely passes the method call through to its own fContent member.

Breaking a link destination

There's one exception to the rule that objects that refer to link destination content across updates must refer to the entire link destination content. Breaking a link destination, which the user accomplishes with the Link Destination Info dialog, causes the previously protected link content to behave as ordinary nonlinked content. The restrictions on operations on partial link content that permitted the convenient hiding of the underlying transient data are no longer in effect. The user can now apply multiple undoable editing operations to random subsets of the content that used to be contained within the link.

If these actions and the breaking of the link destination are then undone, the action data that refers to portions of the linked content will persist, because the breaking of the link and the other undone actions can still be redone. Supposing the user then undoes some action that was performed before breaking the link and that affects the associated link source, then redoes that action, the link destination will be updated twice. What will happen if the user then redoes breaking the link destination and proceeds to attempt to redo some of the actions on portions of the linked content that followed breaking the link? (OK, the user can't make up his mind. It could happen!)

Again, whether this needs attention depends on the content model. References to ranges of text should remain valid after all the undoing and redoing. In LinkingPart, this matters a lot. Individual embedded shapes have to be replaced during each link update, yet somehow must survive to support the above scenario. You may be able to apply an approach similar to that described below

if this issue is likely to be problematic in your part.

The key to LinkingPart's management of this case is that all its content elements and its CLinkingPartContent objects are reference-counted. The following vastly simplified code is executed in the LinkUpdated implementation (in the actual sample code, newContent is initialized from the link's storage unit in a failure-handling block):

CLinkingPartContent* newContent = new CLinkingPartContent(fPart);
newContent->InitLinkingPartContent(ev, su);  // Read from link's storage.
fContent->Removed(ev);                       // Make old stuff disappear.
fContent = newContent;
fContent->Added(ev);                         // "Turn on" the new content.

Typically, the CLinkDestination that's being updated owns the only reference to its fContent. Releasing it makes its reference count go to 0, which causes it to be deleted. This in turn causes the embedded shapes contained in it to be released and deleted, which makes all kinds of permanent things occur, like calling ODFrame::Removed on the embedded frames. But in its constructor, CBreakLinkAction -- the object responsible for initiating, undoing, and redoing breaking a link -- does this:

fLinkContent = fLinkDestination->GetContent();

This protects the link's content from any permanent damage when the link is updated, causing it to behave the same way that content removed by an undoable cut or clear operation would behave. CLinkDestination::BreakLink takes a CLinkingPartContent* argument. It's passed the fLinkContent reference from CBreakLinkAction::Do and CBreakLinkAction::Redo. Before doing whatever is required to make the link content available to the part for unrestricted interaction, BreakLink performs the operation shown in Listing 8.

Listing 8. Safely breaking a link destination

void CLinkDestination::BreakLink(Environment* ev, 
                        CLinkingPartContent* content)
   // If the link updated while the break link was "undone," content
   // (the content when it was first broken) will be different from
   // fContent (the present link content). If so, first substitute the
   // original for the present content.
   if (content != fContent) { 
      // This is similar to a link update, only instead of reading new
      // content from the link, we just use the old content.
      fContent->Removed(ev);       // This will empty our selection.
      fContent = content;

   // Disable the link destination and make whatever is in fContent
   // available as ordinary unlinked content to the part. 

Besides being similar to LinkUpdated, the operation is also identical to what would be done if link updates themselves were undoable and redoable. Our ability to perform this operation transparently to the user depends on the knowledge, which is absent during LinkUpdated but is implied by the design of OpenDoc's undo mechanism when redoing breaking the link, that any link updates that may have occurred while the link wasn't broken must have come in complementary pairs. The resulting content of the link when it's rebroken must at least appear to be the same as its content when the link was originally broken.

When initially breaking the link, or when redoing breaking the link when no updates have occurred, this operation is bypassed because content and fContent remain equivalent. In any case, we're ensured that each time the break link operation is redone, the part content is restored to exactly the same state it was in (with exactly the same embedded frames) when the link was initially broken. Any subsequent actions that had been performed on that data can now be redone safely.

You Can Do It!

We hope that both users and developers will find imaginative ways to take advantage of linking. We would especially like to encourage developers of part editors that support embedding to seriously consider support for linking. The combination of embedding and linking effectively extends a degree of linking capability to all OpenDoc content. It allows users to create links consisting of one or more embedded parts within or between containers that support linking. The content of an embedded frame in a container that supports linking can also be merged into the content of a like part as a link destination if that part supports linking, even if it doesn't support embedding. Conversely, content from one part that supports linking can become the source for an embedded frame link destination in any container that supports linking.

Supporting linking in your part can be easy or challenging, depending on your part's content model. The model represented in LinkingPart and discussed in this article is probably one of the more difficult kinds to handle. Nonetheless, once we thought about it enough and tried a few different approaches, we were able to come up with reasonably simple mechanisms to handle the worst edge cases.

The good news is that a lot of the code you'll need to support linking is already written. Whether you choose to inherit linking behavior from ODF (the recommended route, especially if you're doing embedding, too) or to base your part on the LinkingPart sample code, you can build on what's been provided and focus on the details that will add the most value to your part.

Thanks to our technical reviewers Craig Carper, Dave Curbow, Vincent Lo, and Eric Simenel.

Related Reading

  • "The OpenDoc User Experience" by Dave Curbow and Elizabeth Dykstra-Erickson, develop Issue 22.
  • "Getting Started With OpenDoc Storage" by Vincent Lo, develop Issue 24.
  • OpenDoc Programmer's Guide for the Mac OS by Apple Computer, Inc. (Addison-Wesley, 1995). In particular, see "Linking" (in Chapter 8, on pages 372-400) and "Using Links" (in Part 3, "Human Interface Guidelines," on pages 604-615).
  • OpenDoc Cookbook for the MacOS by Apple Computer, Inc. (Addison-Wesley, 1995).
  • Apple's OpenDoc Web site,

Mike Halpin ( is the principal and chief engineer in the consulting firm of Harmony Engineering, Inc., in Soquel, CA. A software engineer for more than 10 years, he's spent the last two in OpenDoc quality and engineering efforts at Apple, most recently improving support for data transfer and embedding in ODF. A veteran of 12 years on The Farm, an agrarian commune in Tennessee, Mike worked as a plumber, an electrician, a machinist, and a refrigeration mechanic before going on to obtain a B.S. in math in preparation for his present career. He spends his spare time playing the flute and enjoys cooking for family and friends.

Elizabeth Dykstra-Erickson ( is currently the lead human interface designer for OpenDoc. She's been with the project for nearly three years and has worked on various interaction and visual designs including linking. Her expertise is in collaborative software design and design methods. She lives in San Francisco with her husband and two children. Elizabeth also teaches human factors, Web design, and multimedia classes at the University of San Francisco and is pursuing a doctorate in educational technology (leaving just enough time to change the diapers on her newest project, Baby Gunnar, but not enough time to play the piano... oh well).


Community Search:
MacTech Search:

Software Updates via MacUpdate

Notion 2.1.3 - A unified workspace for m...
Notion is the unified workspace for modern teams. Features: Integration with Slack Documents Wikis Tasks More improvements to editing Browse through a page’s images from one place Choose a type... Read more
Cocktail 15.3.7 - General maintenance an...
Cocktail is a general purpose utility for macOS that lets you clean, repair and optimize your Mac. It is a powerful digital toolset that helps hundreds of thousands of Mac users around the world get... Read more
Dropbox 158.4.4564 - Cloud backup and sy...
Dropbox is a file hosting service that provides cloud storage, file synchronization, personal cloud, and client software. It is a modern workspace that allows you to get to all of your files, manage... Read more
WhatsApp 2.2236.10 - Desktop client for...
WhatsApp is the desktop client for WhatsApp Messenger, a cross-platform mobile messaging app which allows you to exchange messages without having to pay for SMS. WhatsApp Messenger is available for... Read more
VOX 3.5.2 - Music player that supports m...
VOX just sounds better! The beauty is in its simplicity, yet behind the minimal exterior lies a powerful music player with a ton of features and support for all audio formats you should ever need.... Read more
TeamViewer 15.34.4 - Establish remote co...
TeamViewer gives you remote control of any computer or Mac over the Internet within seconds, or can be used for online meetings. Find out why more than 200 million users trust TeamViewer! Free for... Read more
ClamXAV 3.5 - Virus checker based on Cla...
ClamXAV is a popular virus checker for OS X. Time to take control ClamXAV keeps threats at bay and puts you firmly in charge of your Mac’s security. Scan a specific file or your entire hard drive.... Read more
Ableton Live 11.2 - Record music using d...
Ableton Live lets you create and record music on your Mac. Use digital instruments, pre-recorded sounds, and sampled loops to arrange, produce, and perform your music like never before. Ableton Live... Read more
Viber 18.5.0 - Send messages and make fr...
Viber lets you send free messages and make free calls to other Viber users, on any device and network, in any country! Viber syncs your contacts, messages and call history with your mobile device, so... Read more
CrossOver 22.0.1 - Run Windows apps on y...
CrossOver can get your Windows productivity applications and PC games up and running on your Mac quickly and easily. CrossOver runs the Windows software that you need on Mac at home, in the office,... Read more

Latest Forum Discussions

See All

TouchArcade Game of the Week: ‘Tallowmer...
With its pre-determined launch date of September 30th it didn’t quite make it in time for our weekly round-up of new game releases, but darn it, I just don’t care. I’m not letting that technicality keep me from picking Tallowmere 2 as our Game of... | Read more »
SwitchArcade Round-Up: Reviews Featuring...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for September 30th, 2022. In today’s article, our pal Mikhail has another review for us. Gosh, that guy needs a day off! He’s taking a look at Let’s Build a Zoo, and it’s not exactly the... | Read more »
Seven Knights 2 recieves global update i...
Netmarble has announced the latest global update for mobile role-playing game Seven Knights 2. The update will bring brand new characters, some very rewarding events, and reworks to a few old favourite heroes. [Read more] | Read more »
‘GRIS+’ Is This Week’s New Apple Arcade...
Nomada Studio and Devolver Digital’s brilliant GRIS+ () has joined Apple Arcade today as an App Store Great. If you’ve not played it yet, GRIS debuted on PC and Nintendo Switch before seeing PS4 and mobile releases later on. I reviewed the iOS... | Read more »
SwitchArcade Round-Up: Reviews Featuring...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for September 29th, 2022. In today’s article, we have a bunch of new releases to check out with the spearhead being The Elder Scrolls V: Skyrim Anniversary Edition. There’s a little here... | Read more »
‘Residual’ Controller Fix Update Release...
A few weeks back developer OrangePixel released their newest game on mobile called Residual. This is a roguelike survival game where you play as a pilot who crash lands on a procedurally generated planet, filled with all sorts of crazy creatures and... | Read more »
‘Queen’s Wish 2: The Tormentor’ Launchin...
We learned back in July that Spiderweb Software’s latest game Queen’s Wish 2: The Tormentor would be heading to desktop platforms in August, which is exactly what happened, and at that time we were guessing that the iOS version of the game would... | Read more »
‘Kingdom Rush Vengeance’ Primal Ravage U...
Ironhide Game Studio’s Kingdom Rush Vengeance ($4.99) has gotten quite a few updates over the years on iOS, Android, and PC. | Read more »
Interactive Film The Gallery screens at...
The FMV/interactive film hybrid video game genre has been around for a while but doesn’t seem to have that many games out there, but they just might get some more exposure as The Gallery has landed a spot in the Dinar Film Festival 2022. [Read... | Read more »
Terraria’s Massive Labor of Love Update...
Re-Logic’s action platforming sandbox game Terraria ($4.99) has just been updated on mobile and console platforms with its newest major update titled the ‘Labor of Love’ update. | Read more »

Price Scanner via

Apple has M1 Mac minis in stock again startin...
Apple has restocked a full line of M1-powered Mac minis available in their Certified Refurbished section starting at only $589 and up to $140 off MSRP. Each mini comes with Apple’s one-year warranty... Read more
13″ M1 MacBook Airs with 16GB of RAM availabl...
Apple has 13″ M1 MacBook Airs (8-Core CPU/7-Core GPU) in stock today with 16GB of RAM for $190 off MSRP, Certified Refurbished. Apple includes a standard one-year warranty with these models, each... Read more
Update: 13-inch Apple M2 MacBook Airs now on...
Amazon has 13″ MacBook Airs with M2 CPUs in stock today and on sale for $150 off MSRP. Shipping is free. Their prices are now $150 off Apple’s MSRP, and they are the lowest prices available for these... Read more
Save $240 on a 14″ 8-core CPU M1 Pro MacBook...
Apple has the 14″ M1 Pro MacBook Pro with 32GB of RAM and a 512GB SSD (Space Gray) in stock for $2159, Certified Refurbished. Regular price for this configuration is $2399, so their savings amounts... Read more
Save $170 on this 13″ M2 MacBook Pro with 16G...
Apple has 13″ M2 MacBook Pros with 16GB of RAM and 512GB SSDs in stock today for $170 off MSRP ($1529), Certified Refurbished. These are the cheapest 13″ M2 MacBook Pros with this configuration... Read more
This Apple retailer is offering a $350 discou...
Apple retailer Expercom is offering a $350 instant discount on select upgraded 16″ Apple MacBook Pros through October 4, 2022. Shipping is free. Their sale price applies to the following... Read more
Clearance 27″ 5K Apple iMacs are on sale star...
Other World Computing has dropped prices on Apple refurbished, factory-sealed, recently-discontinued 27″ 5K iMacs with models now on sale for up to $950 off Apple’s MSRP, starting at only $849. Their... Read more
Get a new Mac for up to $400 off MSRP at Appl...
Need a new Apple Mac for school? Whether you’re a student, teacher, or staff member, you can use your .edu email address when ordering at Apple Education to take up to $400 off the price of a new Mac... Read more
13-inch Apple MacBook Airs with M2 processors...
Amazon has 13″ MacBook Airs with M2 CPUs in stock today and on sale for $1099. Shipping is free. Their prices are $100 off Apple’s MSRP, and they are the lowest prices available for M2-powered Macs... Read more
AR Glasses That Work With Apple’s Hardware? T...
NEWS – Lenovo has created quite the spectacle(s) with its latest product. “Apple Glass” — the purported name of Apple’s forthcoming AR glasses — is not expected to be released until 2025 (at the... Read more

Jobs Board

*Apple* Electronic Repair Technician - PlanI...
…a highly motivated individual to join our Production Department as an Apple Electronic Repair Technician. The computer repair technician will diagnose, assemble, Read more
( *Apple* ) Production Designer/Artist - TEKs...
…Art Ops team supports production, asset management, quality control, and global publishing for Apple Media Products, like Apple Music. The Art Ops team is Read more
Operations Associate - *Apple* Blossom Mall...
Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States ( - Apple Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States ( - Apple Blossom Mall Read more
Sephora Beauty Advisor - *Apple* Blossom Ma...
Sephora Beauty Advisor - Apple Blossom Mall Location:Winchester, VA, United States ( - Apple Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.