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'sGetUndo
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
RedoAction
ReadActionState
WriteActionState
DisposeActionState
UndoAction
andRedoAction
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 whichDisposeActionState
will be called to remove actions that they have placed in the action history.
- WriteActionState and ReadActionState
- The
WriteActionState
andReadActionState
methods ofODPart
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'sAddActionToHistory
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 typeODActionData
, 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 toAddActionToHistory
.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 typeskODBeginAction
andkODEndAction
. (For undoable actions that do not have separate stages, you specify an action type ofkODSingleAction
when callingAddActionToHistory
.)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.
- The source part calls
AddActionToHistory
at the beginning of a drag, specifying an action type ofkODBeginAction
to add a beginning action.- The destination part calls
AddActionToHistory
when the drop occurs, adding a singlestage action to the undo history by specifyingkODSingleAction
.- The source part once again calls
AddActionToHistory
after the completion of the drop (when the drag-and-drop object'sStartDrag
method returns), specifying an action type ofkODEndAction
to add an ending action.
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 withkODBeginAction
and withkODEndAction
, 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.
You can remove an incomplete two-stage action from the undo action
- Menu strings and multistage actions
- The strings you can provide when calling
AddActionToHistory
are ignored if the action type iskODBeginAction
. 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 typekODEndAction
appear in the menu.
history--without having to clear the entire history--by using theAbortCurrentTransaction
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, specifykODRespectMarks
when you call theClearActionHistory
method. To clear the whole action history, specifykODDontRespectMarks
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'sUndoAction
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'sRedoAction
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'sClearActionHistory
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 theDisposeActionState
methods of the parts associated with the cleared actions. BeforeDisposeActionState
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 bekODTrue
.) You set the value of a frame's in-limbo flag by calling itsSetInLimbo
method and passing eitherkODTrue
orkODFalse
. You can determine the value of a frame's in-limbo flag by calling itsIsInLimbo
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 Action Set flag to... If undone, set flag to... If redone, set flag to... When DisposeActionState
is calledCreating a frame (Leave as is) kODTrue
kODFalse
If kODTrue
, callRemove
IfkODFalse
, callRelease
Cutting or clearing a frame kODTrue
kODFalse
kODTrue
If kODTrue
, callRemove
IfkODFalse
, callRelease
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]kODFalse
If kODTrue
and prior value
iskODFalse
, callRemove
Otherwise, callRelease
Pasting or dropping a frame whose containing frame is non-null2 kODFalse
(but save
prior value)(Restore prior value)3 kODFalse
Call Release[9]
Starting a drag kODTrue
(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) kODFalse
kODTrue
If kODTrue
, callRemove
IfkODFalse
, callRelease
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.
Main | Top of Section | What's New | Apple Computer, Inc. | Find It | Feedback | Help