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.
Most systems support only a single-level of Undo; 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. This gives 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 out 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 decide which of your own actions are undoable, which ones are ignored 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 the few.
When you select Redo, the effects of the last Undo are reversed. 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 history and Redo is no longer available until the user chooses Undo once again.
To implement this multilevel Undo that spans different parts, 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.
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 Undo at all.
To implement support for Undo in your part editor, you need to be able to save information to the Undo object that allows you to recover previous states of your part, and your part editor needs to implement the following methods:
The Undo object calls your part's UndoAction and RedoAction methods when you choose, 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.
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 retrieve your undo-related information. The current version of OpenDoc does not support cross-session Undo, however, so you need not override these methods.
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 the state it occupied 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 perform an Undo.
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 have 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, which is 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. Opening or closing a document, or performing ignorable actions such as scrolling or selecting, should not cause you to add an action to the action history.
You decide what constitutes a single action. Entering of 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.
Some transactions, such as drag-and-drop, have 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 you select the Paste with link checkbox in the Paste As dialog box (see Figure 63), the part receiving the paste and creating the link destination adds the begin action and end 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 single-stage actions to the undo history during the time that a two-stage 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 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".
The undoable actions a user performs while a modal dialog box is displayed should not affect the action history previous to that modal state unless the actions clear the undo action history. After dismissing the dialog box, the user should be able to undo actions performed before the modal dialog box was displayed
To implement this behavior, you can put a mark into the action history that specifies 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, you specify kODRespectMarks when you call the ClearActionHistory method. To clear the whole action history, specify kODDontRespectMarks instead.
For every time you put a mark into the action history, you must make sure there is an equivalent call to ClearActionHistory to clear that subhistory.
When you choose 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 involved, so that the entire compound action is reversed).
When you choose to Redo an action of your part, OpenDoc calls your part's RedoAction method, passing it the same action data that you had passed in earlier when the original action was performed. In this case, your task is to re-achieve 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 involved, so that the entire compound action is reversed).
As soon as your part performs an action that you decide cannot be undone, it must clear 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. Undoable actions such as normal editing and performing 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 single actions form the undo stack, without clearing the entire action history.
OpenDoc itself clears the action history when it closes a document.
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). When a frame is initially created, whether through CreateFrame or by cloning, its in-limbo flag is cleared (kODFalse).
Whenever your part cuts, pastes, drags, or drops an embedded frame, you must keep a reference to that frame in an undo action. When you first perform the data transfer involving the frame, and when you perform subsequent actions with the frame (including undo and redo), you must set the frame's in-limbo flag accordingly. OpenDoc requires the correct flag settings to properly handle moved and deleted objects.
Table 5
shows how to set the 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 5. Setting a Frame's In-Limbo Flag
Action | Set flag to... |
If Undone,
set flag to...
|
If Redone,
set flag to...
|
When
DisposeActionState
is called
|
---|---|---|---|---|
Creating a frame | (leave as is) | kODTrue | kODFalse |
If kODTrue, call Remove;
If kODFalse, call Release
|
Deleting, cutting, or starting a drag-move* | kODTrue | kODFalse | kODTrue |
If kODTrue, call Remove;
If kODFalse, call Release
|
Copying a frame | (leave as is) | (Note1) | (Note1) | (Note1) |
Pasting or dropping a moved (not copied) frame |
kODFalse
(but save prior value)
| (restore prior value) |
kODFalse
(but save prior value)
|
If kODFalse, call Release;
If prior value = kODTrue,
call Release;
Otherwise, call Remove
|
When StartDrag returns |
kODFalse
(if drag was a copy
or if drop failed;
else leave as is)
| (leave as is) |
kODFalse
(if drag was a copy
or if drop failed;
else leave as is)
|
(nothing)
|
Note1: Do not create an undo action for this situation.
* "Starting a drag-move" in this case means starting a drag in any situation in which a move is a possible outcome.
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.
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.