#import "UndoManager.h"
/******************************************************************************
UndoManager
The UndoManager object is used to handle undo and redo on a document basis in an application. Objects in the application register changes to themselves by sending an "undo" message to an undoManager instance from within an undoable method.For Example:
- setRadius:(float)value
{
// Give undoManager the target for this event, then send undo message.
[[undoManager setUndoTarget:self] setRadius:radius];
// Set the new value and return
radius = value; return self;
}
The UndoManager keeps a group (or list) of records for each "undoable" change. Each record contains a pointer to an object, an action for that object to perform and the arguments for that action. Undo is achieved by executing the target/action information in these undo records.
To add several records to an undo group, enclose the registration messages between UndoManager's -beginUndoRecordGrouping and -endUndoRecordGrouping methods. Undo messages that are not surrounded by these methods are added to the undoList in a group by themselves.
Also since sometimes changes should be ignored (ie. all of the discrete movements of a shape in a drag loop should not be undoable, just the final overall move), the messages -disableUndoRegistration and -reenableUndoRegistration can be sent to control which events are accepted into an undo record. For example, you might call [undoManager disableUndoRegistration] on a mouseDown: to prevent methods that register themselves with the undoManager to register each mouse drag, then you would call [undoManager reenableUndoRegistration] on mouseUp: with another call that would undo the drag loop (ie, [undoManager moveSelectedObjectsTo:oldPoint]).
The UndoManager allows the User to set the number of levels of undo to maintain (-setLevelsOfUndo:). When the number of records exceeds this amount, the excess is removed from the end of the list.
The UndoManager can be instructed to free target or argument data when records are removed from the undoList (whether they fall off the end of the list or are executed during an undo command). This is achieved by the -freeUndoTarget and -freeUndoArgs methods. Additionally the -copyUndoArgs method will copy any pointers that are registered with the next message (it is then assumed that the UndoManager will free them).
To trigger an Undo record, simply call -undo. This places the UndoManager into a "redo" state. All messages that are registered during an Undo are kept in a "redo" list. Thus the undo manager provides for redo as well. Redo can be triggered with the -redo message. The redo list is emptied each time a new record is added to the undo list (because, redo should only work if there were previous undos).
Clients of the undoManager can be notified when an undo is executed by adding themselves to the UndoManager's delegate list(-addUndoDelegate:). The only notification messages are -undoManagerWillUndo: and -undoManagerDidUndo: (called before and after an undo or redo). This notification is useful to do perform functions that all undo events may require, like calling -display.
Written by: Jeff Martin (jmartin@bozell.com)
You may freely copy, distribute and reuse the code in this example.
Don't even talk to me about warranties.
******************************************************************************/
#ifdef hppa
#define TARGET(UndoRec) (*(id *)(UndoRec->args+UndoRec->argSize-4))
#define SELECTOR(UndoRec) (*(SEL *)(UndoRec->args+UndoRec->argSize-8))
#else
#define TARGET(UndoRec) (*(id *)UndoRec->args)
#define SELECTOR(UndoRec) (*(SEL *)(UndoRec->args+4))
#endif
#define BIT(x) (((unsigned int)1)<<(x))
#define BITS(from, to) ((((BIT(to) - 1)<<1) + 1) & !(BIT(from) - 1))
id undoManager = NULL;
@implementation UndoManager
- init
{
[super init];
undoList = [[List allocFromZone:[self zone]] init];
redoList = [[List allocFromZone:[self zone]] init];
delegateList = [[List allocFromZone:[self zone]] init];
disabled = NO; undoing = NO; redoing = NO;
levelsOfUndo = 99;
target = NULL; freeArgsMask = copyArgsMask = 0;
return self;
}
/******************************************************************************
- beginUndoRecordGrouping, - endUndoRecordGrouping
These methods allow the client of the UndoManager to place several undo records in one undo "event". Undo messages that are received while not surrounded by begin/end undoRecordGrouping are placed into the undo list individually.
******************************************************************************/
- beginUndoRecordGrouping
{
if((!recordGrouping++) && (!disabled)) {
// Get the appropriate list
id list = undoing? redoList :undoList;
id recordGroup = [[Storage allocFromZone:[self zone]] initCount:0
elementSize: sizeof(UndoRecord) description:@encode(UndoRecord)];
// Add new recordGroup to the list if there is not an empty one there
if(![list objectAt:0] || [[list objectAt:0] count]) {
[list insertObject:recordGroup at:0];
// If list has more elements than levelsOfUndo, remove one from end
if([list count] > levelsOfUndo) [self discardRecordGroup:
[list removeObjectAt:[list count]-1]];
}
}
return self;
}
- endUndoRecordGrouping { recordGrouping--; return self; }
/******************************************************************************
- disableUndoRegistration, - reenableUndoRegistration
These methods allow the undomanager to ignore events. This is useful in a case like mouse dragging, where the application only wants the final move to be undoable and not all of the intermediate drags. These should be strategically placed in blocks and can be nested.
******************************************************************************/
- disableUndoRegistration { disabled++; return self; }
- reenableUndoRegistration { disabled--; return self; }
/******************************************************************************
- setUndoTarget:object
This method sets the target of following undo records. This is necessary because the sender cannot be gleaned from the forward method. Returns self.
******************************************************************************/
- setUndoTarget:object { target = object; return disabled? NULL : self; }
/******************************************************************************
- freeUndoTarget, - freeUndoArgs, - freeUndoArgAt:(int)pos
These methods specify whether the UndoManager should free the target and/or its args when an undo record is executed or when it exceeds the number of levels of undo. These methods set bits in the freeArgsMask representing the argument position. Position 0 is the target. Position 1-15 represent the first 15 arguments to the method. Bits 0-15 in the freeArgsMask represent args to be freed when the record is discarded, while Bits 16-31 represent args to be freed when the record is executed.
******************************************************************************/
- freeUndoTarget { freeArgsMask |= BIT(0)|BIT(16); return self; }
- freeUndoArgs { freeArgsMask |= ((-1) & (!BIT(0)) & (!BIT(16))); return self; }
- freeUndoArgAt:(int)pos { freeArgsMask |= (BIT(pos)|BIT(pos+16));return self; }
/******************************************************************************
- freeUndoTargetOnRecordDiscard, - freeUndoArgsOnRecordDiscard,
- freeUndoArgOnRecordDiscardAt:(int)pos
These methods specify whether the UndoManager should free the target and/or its args when an undo record exceeds the number of levels of undo. These methods set bits 0 - 15 in the freeArgsMask representing the argument position. Position 0 is the target. Position 1-15 represent the first 15 arguments to the method.
******************************************************************************/
- freeUndoTargetOnRecordDiscard { freeArgsMask |= BIT(0); return self; }
- freeUndoArgsOnRecordDiscard { freeArgsMask |= BITS(1,15); return self; }
- freeUndoArgOnRecordDiscardAt:(int)pos
{ freeArgsMask |= BIT(pos); return self; }
/******************************************************************************
- freeUndoTargetOnRecordExecute, - freeUndoArgsOnRecordExecute,
- freeUndoArgOnRecordExecuteAt:(int)pos
These methods specify whether the UndoManager should free the target and/or its args when an undo record is executed. These methods set bits 16 - 31 in the freeArgsMask representing the argument position. Position 16 is the target. Position 17-31 represent the first 15 arguments to the method.
******************************************************************************/
- freeUndoTargetOnRecordExecute { freeArgsMask |= BIT(16); return self; }
- freeUndoArgsOnRecordExecute { freeArgsMask |= BITS(17,31); return self; }
- freeUndoArgOnRecordExecuteAt:(int)pos
{ freeArgsMask |= BIT(pos+16); return self; }
/******************************************************************************
- copyUndoArgs, - copyUndoArgsAt:(int)pos
copyUndoArgs and copyUndoArgsAt: tell the UndoManager to copy pointer arguments(like objects or strings) for convenience. It is assumed that the UndoManager will free them when the undoRecord is freed. Returns self.
copyUndoArgsFreeOnDiscard and copyUndoArgsFreeOnExecute are just like copyUndoArgs but they specify whether to free the args when the record is discarded or when it is executed. They all Return self.
******************************************************************************/
- copyUndoArgs { copyArgsMask = -1; [self freeUndoArgs]; return self; }
- copyUndoArgAt:(int)pos
{ copyArgsMask |= BIT(pos); [self freeUndoArgAt:pos]; return self; }
- copyUndoArgsFreeOnDiscard
{ [self copyUndoArgs]; [self freeUndoArgsOnRecordDiscard]; return self; }
- copyUndoArgFreeOnDiscardAt:(int)pos
{ [self copyUndoArgAt:pos];[self freeUndoArgOnRecordDiscardAt:pos];return self;}
- copyUndoArgsFreeOnExecute
{ [self copyUndoArgs]; [self freeUndoArgsOnRecordDiscard]; return self; }
- copyUndoArgFreeOnExecuteAt:(int)pos
{ [self copyUndoArgAt:pos];[self freeUndoArgOnRecordDiscardAt:pos];return self;}
/******************************************************************************
- copyUndoArgsForRecord:(UndoRecord *)undoRecord
copyUndoArgsForRecord is used internally and does the actual work of copying the pointers.
******************************************************************************/
- copyUndoArgsForRecord:(UndoRecord *)undoRecord
{
Method method = class_getInstanceMethod([TARGET(undoRecord) class],
SELECTOR(undoRecord));
int argCount = method_getNumberOfArguments(method);
int i, offset;
char *type;
// Copy the target if requested
if(copyArgsMask & BIT(0)) TARGET(undoRecord) =
[TARGET(undoRecord) copyFromZone:[self zone]];
// Copy each argument that the client requested
for(i=2; i<argCount; i++) if(copyArgsMask & BIT(i-1)) {
// Get argument type and offset
method_getArgumentInfo(method, i, &type, &offset);
#ifdef hppa
offset += undoRecord->argSize;
#endif
// If argument is a string copy with NXCopyStringBufferFromZone
if(!strncmp(type,"*",1)) {
char **stringPtr = (char **)(undoRecord->args+offset);
*stringPtr = (*stringPtr)?
NXCopyStringBufferFromZone(*stringPtr, [self zone]) : NULL;
}
// If argument is an object copy with -copyFromZone:
if(!strncmp(type,"@",1)) {
id *objectPtr = (id *)(undoRecord->args+offset);
*objectPtr = [*objectPtr copyFromZone:[self zone]];
}
}
return self;
}
/******************************************************************************
- forward:(SEL)aSelector :(marg_list)argFrame
This method is how events are registered with the Undo Manager. When the UndoManager receives a message that it doesn't respond to, it assumes that this is an undo event and associates it with the current target. This undoRecord is then added to the undoList.
******************************************************************************/
- forward:(SEL)aSelector :(marg_list)argFrame
{
// Register the event if we are not disabled and there is a current target
if((!disabled) && target) {
// Allocate a new undo record and set its fields
UndoRecord *undoRecord = NXZoneMalloc([self zone], sizeof(UndoRecord));
int argSize = method_getSizeOfArguments(
class_getInstanceMethod([target class], aSelector));
undoRecord->freeArgsMask = freeArgsMask;
undoRecord->argSize = argSize;
// Make a copy of the arguements (copy deep if requested)
undoRecord->args = NXZoneMalloc([self zone], argSize);
#ifdef hppa
bcopy(argFrame - argSize, undoRecord->args, argSize);
#else
bcopy(argFrame, undoRecord->args, argSize);
#endif
TARGET(undoRecord) = target;
if(copyArgsMask) [self copyUndoArgsForRecord:undoRecord];
// Do implicit recordGrouping if needed
if(!recordGrouping) [[self beginUndoRecordGrouping]
endUndoRecordGrouping];
if(undoing) [[redoList objectAt:0] addElement:undoRecord];
else [[undoList objectAt:0] addElement:undoRecord];
// Make sure that redo list is empty if in normal mode.
if(!undoing && !redoing) while([redoList count])
[self discardRecordGroup:[redoList removeObjectAt:0]];
}
// Reset registration masks
freeArgsMask = 0; copyArgsMask = 0;
return self;
}
/******************************************************************************
- undo:sender
This message actually triggers the UndoManager to take the top undo record and send out all of the events. The record is then freed.
******************************************************************************/
- undo:sender
{
id recordGroup = [undoList removeObjectAt:0];
// Get the last list that actually had undo records in it
while(recordGroup && ![recordGroup count])
recordGroup = [undoList removeObjectAt:0];
// Beep if there was no record group
if(!recordGroup) NXBeep();
// Otherwise set undo flag, execute record group, reset undo flag & return
else { undoing = YES; [self executeRecordGroup:recordGroup]; undoing = NO; }
return self;
}
/******************************************************************************
- redo
This message actually triggers the UndoManager to take the top redo record and send out all of the events. The record is then freed.
******************************************************************************/
- redo:sender
{
id recordGroup = [redoList removeObjectAt:0];
// Get the last list that actually had undo records in it
while(recordGroup && ![recordGroup count])
recordGroup = [redoList removeObjectAt:0];
// Beep if there was no record group
if(!recordGroup) NXBeep();
// Otherwise set redo flag, execute record group, reset redo flag & return
else { redoing = YES; [self executeRecordGroup:recordGroup]; redoing = NO; }
return self;
}
/******************************************************************************
- (int)levelsOfUndo, - setLevelsOfUndo:(int)value
These methods allow programmatic manipulation for the levels of undo that an UndoManager maintains.
******************************************************************************/
- (int)levelsOfUndo { return levelsOfUndo; }
- setLevelsOfUndo:(int)value { levelsOfUndo = value; return self; }
/******************************************************************************
- addUndoDelegate:object, - removeUndoDelegate:object
These methods add and remove objects that are to receive undo notification. The two notifications that are currently supported are undoManagerWillUndo: and undoManagerDidUndo (sent before and after an Undo or Redo).
******************************************************************************/
- addUndoDelegate:object { [delegateList addObject:object]; return self; }
- removeUndoDelegate:object { [delegateList removeObject:object]; return self; }
- sendNotification:(SEL)action
{
int i; for(i=0; i<[delegateList count]; i++)
if([[delegateList objectAt:i] respondsTo:action])
[[delegateList objectAt:i] perform:action with:self];
return self;
}
/******************************************************************************
- discardRecordGroup:recordGroup;
- executeRecordGroup:recordGroup;
- freeUndoRecord:(UndoRecord *)undoRecord;
These methods free the individual records in a record group. discardRecordGroup: calls freeUndoRecord:withMask: with the discard part of the freeArgsMask while executeRecordGroup: executes the records in the record group then calls freeUndoRecord:withMask: with the execute part of the the freeArgsMask.
freeUndoRecord walks through the record arguments free those that the freeMask requests to be freed. Returns self.
******************************************************************************/
- discardRecordGroup:list
{
int i;
// Free the records in the list, then the list
for(i=0; i<[list count]; i++)
[self freeUndoRecord:[list elementAt:i] withFreeMask:freeArgsMask];
[list free];
return self;
}
- executeRecordGroup:recordGroup
{
int i;
// Send WillUndo notification
[self sendNotification:@selector(undoManagerWillUndo:)];
// Start undo record grouping, execute & free records & stop record grouping
[self beginUndoRecordGrouping];
for(i=0; i<[recordGroup count]; i++) {
UndoRecord *rec = [recordGroup elementAt:i];
#ifdef hppa
[TARGET(rec) performv:SELECTOR(rec) :rec->args + rec->argSize];
#else
[TARGET(rec) performv:SELECTOR(rec) :rec->args];
#endif
[self freeUndoRecord:rec withFreeMask:(rec->freeArgsMask>>16)];
}
[self endUndoRecordGrouping];
// Send DidUndo notification and return
[self sendNotification:@selector(undoManagerDidUndo:)];
return self;
}
- freeUndoRecord:(UndoRecord *)undoRecord withFreeMask:(int)freeMask
{
Method method = class_getInstanceMethod([TARGET(undoRecord) class],
SELECTOR(undoRecord));
int argCount = method_getNumberOfArguments(method);
int i, offset;
char *type;
// Free the target if requested
if(freeMask & BIT(0)) [TARGET(undoRecord) free];
// Free anything that the args point to if requested
for(i=2; i<argCount; i++) if(freeMask & BIT(i-1)) {
method_getArgumentInfo(method, i, &type, &offset);
#ifdef hppa
offset += undoRecord->argSize;
#endif
// Free any strings or pointers with free() function
if(!strncmp(type,"*",1)) free(*(char **)(undoRecord->args+offset));
// Free any objects with -free method
else if(!strncmp(type,"@",1)) [*(id *)(undoRecord->args+offset) free];
![]()
// Free the args
free(undoRecord->args);
return self;
}
/******************************************************************************
- emptyUndoManager
emptyUndoManager removes all of the recordGroups from the undoList and redoList. An UndoManager client might do this when a document is saved. Returns self.
******************************************************************************/
- emptyUndoManager
{
while([undoList count]) [self discardRecordGroup:
[undoList removeObjectAt:0]];
while([redoList count]) [self discardRecordGroup:
[redoList removeObjectAt:0]];
return self;
}
/******************************************************************************
- free
Frees each undo group in the undo and redo lists then it frees the undo and redo lists.
******************************************************************************/
- free
{
[self emptyUndoManager];
[undoList free]; [redoList free]; [delegateList free];
return [super free];
}
@end