Title Banner

Previous Book Contents Book Index Next

Inside Macintosh: OpenDoc Programmer's Guide / Part 2 - Programming
Chapter 6 - Windows and Menus


Undo

The Undo command (and its reverse, Redo) is a common feature on many software platforms. It is designed to allow users to recover from errors that they recognize soon after making them. OpenDoc offers a flexible and powerful undo capability.

Multilevel, Cross-Document Capability

Most systems support only a single-level Undo command; that is, only the most recently executed command can be reversed. Therefore, in most platforms undo is restricted to a single domain. A complex operation that involves transferring data from one document to another, for example, cannot be completely undone.

OpenDoc, by contrast, supports multiple levels of undo and redo; there is no limit, other than memory constraints on the user's system, to the number of times in succession that a user can invoke the Undo command. OpenDoc also allows for interdocument undo and redo. Together, these enhancements give users greater flexibility in recovering from errors than is possible with simpler undo capabilities. �

The OpenDoc undo feature does not offer infinite recoverability. Some user actions clear the undo action history, the cumulative set of reversible actions available at any one time, resetting it to empty. A typical example is saving a document; the user cannot "unsave" the document by choosing Undo, and no actions executed prior to its saving can be undone. As a part developer, you can decide which of your own actions are undoable, which ones are ignorable for undo purposes, and which ones reset the action history. In general, actions that do not change the content of a part (such as scrolling, making selections, and opening and closing windows) are ignorable and do not need to be undoable. Non-undoable actions that require clearing the action history are few; closing your part (see "Closing Your Part") is one of them.

Choosing Redo reverses the effects of the previous Undo command. Redo is available only if the user has previously chosen Undo, and if nothing but ignorable actions have occurred since the user last chose Undo or Redo. Like Undo, Redo can be chosen several times in succession, limited only by the number of times Undo has been chosen in succession. As soon as the user performs an undoable action (such as an edit), OpenDoc clears the redo action history and the Redo command is no longer available until the user chooses Undo once again.

To implement this multilevel undo capability that spans different parts and different documents, OpenDoc maintains a centralized repository of undoable actions, stored by individual part editors as they perform such actions. Undo history is stored in the undo object, an instantiation of the class ODUndo, created by OpenDoc and accessed through the session object's GetUndo method. Your part editor needs access to the undo object to store undoable actions in it or retrieve them from it.

The importance of multilevel undo
Your part must support multiple levels of undo. If your part supports only a single-level Undo command, other parts that support multilevel undo will lose their undo history when they interact with your part. You should support multiple levels of undo if you support the Undo command at all.

Implementing Undo

To implement support for undo in your part editor, you need to save informa-
tion to the undo object that allows you to recover previous states of your part. Also, your part editor needs to implement the following methods:

The undo object calls your part's UndoAction and RedoAction methods when the user chooses, respectively, the Undo and Redo items from the Edit menu.

The undo object calls your part's DisposeActionState method when an undo action of yours is removed from the undo action history. At that point, you can dispose of any storage needed to perform the specified undo action. Parts should not make any assumptions about the order in which DisposeActionState will be called to remove actions that they have placed in the action history.

WriteActionState and ReadActionState
The WriteActionState and ReadActionState methods of ODPart exist to support a future cross-session undo capability. OpenDoc can call these methods when it needs you to store persistently or to retrieve your undo-related information. Version 1.0 of OpenDoc does not support cross-session undo, however, so you need not override these methods.

Adding an Action to the Undo Action History

If your part performs an undoable action, it should call the undo object's AddActionToHistory method. Your part passes an item of action data to the method; the action data contains enough information to allow your part to revert to its state just prior to the undoable action. The action data can be in any format; OpenDoc adds it to the action history in the undo object and passes it back to your part if the user asks your part to undo a command.

The item of data that you pass to AddActionToHistory must be of type ODActionData, which is a byte array. When you create the item, you can either copy the data itself into the byte array or you can copy a pointer to the data into the byte array; either way, you then pass the byte array to AddActionToHistory.

You also pass two user-visible strings to the AddActionToHistory method. The strings consists of the text that you want to appear on the Edit menu, such as "Undo Cut" and "Redo Cut". You must also specify the data's action type, described under "Creating an Action Subhistory".

In general, you add most editing actions to the action history unless their data is so large that you cannot practically recover the pre-action state. Actions such as opening or closing a document, or ignorable actions such as scrolling or selecting, should not be added the action history.

You decide what constitutes a single action. Entering an individual text character is not usually considered an action, although deleting or replacing a selection usually is. The sum of actions performed between repositionings of the insertion point is usually considered a single undoable action.

Adding Multistage Actions

Some transactions, such as drag and drop, have at least two stages--a beginning and an end. To add such a transaction to the undo history, your part must define which stage you are adding, by using the action types kODBeginAction and kODEndAction. (For undoable actions that do not have separate stages, you specify an action type of kODSingleAction when calling AddActionToHistory.)

In the case of drag and drop, the sequence is like this:

Similarly, if the user selects the Paste With Link checkbox in the Paste As dialog box (see Figure 8-3), the part receiving the paste and creating the link destination adds the beginning action and ending action, whereas the source part adds a single action when it creates the link source.

As the case of drag and drop demonstrates, parts can add actions to the undo history during the time that a multistage undoable action is in progress--that is, between the times AddActionToHistory is called with kODBeginAction and with kODEndAction, respectively. These actions become part of the overall undoable action. The added actions can themselves be singlestage or multistage. If an added action is multistage, it must be completely nested within the outer multistage action; the nested action must end before the outer action does.

Menu strings and multistage actions
The strings you can provide when calling AddActionToHistory are ignored if the action type is kODBeginAction. Furthermore, any strings you provide for subsequent single actions are ignored. Once you have created a beginning action, only the strings passed for the action type kODEndAction appear in the menu.
You can remove an incomplete two-stage action from the undo action
history--without having to clear the entire history--by using the AbortCurrentTransaction method. See "Clearing the Action History"

Creating an Action Subhistory

In general, the undoable actions a user performs while a modal dialog box is displayed--as long as the actions do not clear the undo action history of your document--should not affect the action history previous to that modal state. After dismissing the dialog box, the user should be able to undo actions performed before the modal dialog box was displayed--even though at this point the user could not undo actions taken while the modal dialog box was open.

To implement this behavior, you can put a mark into the action history that signifies the beginning of a new action subhistory. To do so, call the MarkActionHistory method of the undo object. To clear the actions within a subhistory, specify kODRespectMarks when you call the ClearActionHistory method. To clear the whole action history, specify kODDontRespectMarks instead.

For every mark you put into the action history, you must put an equivalent call to ClearActionHistory to clear that subhistory.

Undoing an Action

When the user chooses to undo an action added to the action history by your part, OpenDoc calls your part's UndoAction method, passing it the action data you passed in earlier. Your part should perform any reverse editing necessary to restore itself to the pre-action state. (When a two-stage transaction involving your part is undone, OpenDoc calls both your part and the other part or parts involved, so that the entire compound action--including any singlestage actions that occurred between the beginning and ending actions--is reversed.)

Your part's UndoAction method should show the user the effect of the undo action in context. For example, if the undo action affects text that is scrolled out of view, your method should bring the affected window to the front and scroll through the text so that the user can see the change.

Redoing an Action

When the user chooses to redo an action of your part, OpenDoc calls your part's RedoAction method, passing it the same action data you passed in earlier, when the original action was performed. In this case, your task is to restore the state represented by the action data, not reverse it. (When a two-stage transaction involving your part is redone, OpenDoc calls both your part and the other part or parts involved, so that the entire compound action--including any singlestage actions that were undone between the beginning and ending actions--is restored.)

Your part's RedoAction method should show the user the effect of the redo action in context. For example, if the redo action affects text that is scrolled out of view, your method should bring the affected window to the front and scroll through the text so that the user can see the change.

Clearing the Action History

As soon as your part performs an action that you decide cannot be undone, OpenDoc clears the action history by calling the undo object's ClearActionHistory method. Actions that cannot be undone, and for which the action history should be cleared, include saving a changed document and performing an editing operation that is too large to be practically saved. Ignorable actions, such as scrolling or selecting, should not affect the action history.

If your part initiates a two-stage action and, because of an error, must terminate it before adding the second stage to the action history, it can remove the incomplete undo action by calling the undo object's AbortCurrentTransaction method. AbortCurrentTransaction clears the beginning action and any subsequent added actions from the undo stack without clearing the entire action history.

Calling AbortCurrentTransaction triggers calls to the DisposeActionState methods of the parts associated with the cleared actions. Before DisposeActionState is called, the parts are asked to undo the actions with a call to the parts' UndoAction method.

OpenDoc itself clears the action history when it closes a document.

Undo and Embedded Frames

Every frame has a flag, the in-limbo flag, that determines whether the frame (and any of its embedded frames) is actually part of the content of its draft or whether it is currently "in limbo" (referenced only in undo action data). The in-limbo flag allows parts that transfer an embedded frame to ensure that the frame is eventually removed from the part it displays when the frame can no longer be used. If your part editor does not support embedding, you do not have to worry about in-limbo flags.

A frame is in limbo only when it is no longer logically connected to the document content but is still referenced by the part in which it had been embedded. (An exception is that, during a drag, a frame can be marked as in limbo even though it is still embedded in the part initiating the drag.) For example, a frame that has been cut is in limbo until the cut is finalized by disposing of the undo action for the cut; at that time, the frame is irrevocably removed from the part it previously displayed. (Cutting a frame does not necessarily cut the part it displays; the part may still be displayed in other frames.) A frame that is not currently connected to a part is not in limbo as long as it is still logically part of the document layout. In a typical document, many frames may not be visible. Even though these frames have no facets and are not in memory, they are not in limbo.

Whenever your part deletes, cuts, pastes, drags, or drops an embedded frame, you must keep a reference to that frame in an undo action. You must set the frame's in-limbo flag accordingly when you first perform the data transfer involving the frame, when you perform subsequent actions with the frame (including undo and redo), and when you commit that action in your DisposeActionState method. OpenDoc cannot handle moved and deleted object properly unless these flag settings are correct.

When a frame is initially created, whether through CreateFrame or by cloning, its in-limbo flag is cleared. (However, cloning doesn't always create new frames; during a drag-move, for example, cloning can deliver an existing frame. In this case, the frame's in-limbo flag can be kODTrue.) You set the value of a frame's in-limbo flag by calling its SetInLimbo method and passing either kODTrue or kODFalse. You can determine the value of a frame's in-limbo flag by calling its IsInLimbo method. You set the in-limbo flag of frames in your part's draft only, not of frames in data-interchange objects.

Table 6-4 shows how to set the in-limbo flag properly for each situation and how to dispose of the frame referenced in your undo action when your part's DisposeActionState method is called.
Table 6-4 Setting a frame's in-limbo flag
ActionSet flag to...If undone, set flag to...If redone, set flag to...When DisposeActionState
is called
Creating a frame(Leave as is)kODTruekODFalseIf kODTrue, call Remove
If kODFalse, call Release
Cutting or clearing a framekODTruekODFalsekODTrueIf kODTrue, call Remove
If kODFalse, call Release
Copying a frame(Leave as is)(Do not create an undo action for this situation)
Pasting or dropping a frame whose containing frame is null[7]kODFalse
(but save
prior value)
kODTrue[8]kODFalseIf kODTrue and prior value
is kODFalse, call Remove
Otherwise, call Release
Pasting or dropping a frame whose containing frame is non-null2kODFalse
(but save
prior value)
(Restore prior value)3kODFalseCall Release[9]
Starting a dragkODTrue (See "When StartDrag returns", first column)
When StartDrag returns (drop failed)kODFalse (Abort the undo transaction in this situation)
When StartDrag returns (drag was a copy)kODFalse (Leave as is)(Leave as is)(Nothing)
When StartDrag returns (drag was a move)[10](Leave as is)kODFalsekODTrue If kODTrue, call Remove
If kODFalse, call Release

When you re-embed a frame in performing an undo or redo action, you must also reset its link status appropriately. See "Frame Link Status" for more information.

IMPORTANT
When undoing a paste or drop that involved a link destination, the link may have updated, replacing some of the embedded frames. The characteristics of those frames may be different from those of the frames that were initially dropped (specifically, their in-limbo values may have changed). You cannot assume that all the frames affected by the undoing or disposal of a drop or paste should be treated identically. You need to cache this information and select the appropriate undo, redo, and dispose actions on a frame-by-frame basis.

[7] Make this determination after completing the cloning transaction but before embedding the frame.
[8] In addition to handling the in-limbo flag as outlined in this table, your part is responsible for restoring a frame to its original state when a paste or drop is undone. Your part needs to do this only for cloned frames, not for frames it creates, because these can never have been in use elsewhere. The frame characteristics that must be restored are those determined by the containing part, such as the containing frame and the frame shape.
[9] This case can be implemented using the more complex logic for the root frame case. However, because of the way the in-limbo flag is set in this row, DisposeActionState will always release the frame.
[10] Although the part starting a drag must wait until StartDrag returns to determine that a drag was a move, the operations in this row must be associated with the undo action added before the drag was started. This way, if the action is undone, for example, the drop will be undone before the drag is undone.

Previous Book Contents Book Index Next

© Apple Computer, Inc.
16 JUL 1996




Navigation graphic, see text links

Main | Top of Section | What's New | Apple Computer, Inc. | Find It | Feedback | Help