/*	MiscUndoManager.m

	Copyright 1996 Uwe Hoffmann.

	This notice may not be removed from this source code.
	The use and distribution of this software is governed by the
	terms of the MiscKit license agreement.  Refer to the license
	document included with the MiscKit distribution for the terms.

	Version 2 (August 1996)
*/

#import "MiscUndoManager.h"


@interface MiscUndoGroup:NSObject
{
	NSMutableArray *elements;
	NSString *undoName;
	NSString *redoName;
}

+ (MiscUndoGroup *)groupWithUndoName:(NSString *)undoName redoName:(NSString *)redoName;
- initWithUndoName:(NSString *)undoName redoName:(NSString *)redoName;
- (NSString *)undoName;
- (NSString *)redoName;
- (void)addUndoRecord:(NSInvocation *)record;
- (void)dispatch;

@end

@implementation MiscUndoGroup

+ (MiscUndoGroup *)groupWithUndoName:(NSString *)uName redoName:(NSString *)rName
{
	MiscUndoGroup *theGroup;
	
	theGroup = [[MiscUndoGroup alloc] initWithUndoName:uName redoName:rName];
	return [theGroup autorelease];
}

- initWithUndoName:(NSString *)uName redoName:(NSString *)rName
{
	[super init];
	undoName = [uName copyWithZone:[self zone]];
	redoName = [rName copyWithZone:[self zone]];
	elements = [[NSMutableArray allocWithZone:[self zone]] initWithCapacity:10];
	return self;
}

- (void)dealloc
{
	[undoName release];
	[redoName release];
	[elements release];
	return [super dealloc];
}
	
- (NSString *)undoName
{	
	return undoName;
}

- (NSString *)redoName
{
	return redoName;
}

- (void)addUndoRecord:(NSInvocation *)record
{
    	[record retainArguments];
	[elements addObject:record];
}

- (void)dispatch
{
	[elements makeObjectsPerform:@selector(invoke)];
}

@end
	
@implementation MiscUndoManager
/*"
Objects of the class MiscUndoManager provide an easy way to handle undo and redo actions. Clients of  an MiscUndoManager instance make a method undoable by registering  within the method an appropiate NSInvocation instance with the MiscUndoManager instance. Registering is done by sending the message #registerRecord: with the NSInvocation instance as argument. Registering an NSInvocation instance adds it to the current group of records. Undone is always a whole group of records. The message #startUndo:: starts a new group and sending #commitUndo adds the group to the undoArray ready to be undone.

Sending the MiscUndoManager instance the message #undo will undo the last commited group and put the MiscUndoManager instance in a "redo" state. Every group of records commited in this state are added to the redoArray thus providing an redo mechanism. 

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 methods #disableUndoRegistration and #reenableUndoRegistration can be sent to control the registration process. 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 registerRecord:aMoveInvocation] ).

There are two ways to construct the appropiate NSInvocation instances. The two ways will be illustrated below with an example. Suppose we have
a class Circle with instance variables %radius (a float) and %undoManager (an MiscUndoManager instance) and the undoable method #{-(void)setRadius:(float)aRadius}.
   
   	1.) Let the undoManager construct the NSInvocation instance.
            You send the undoManager the message that will be called in the undo process and the undoManager
            constructs the NSInvocation instance and registers it. Keep in mind two things: the message must be a message not implemented by
            MiscUndoManager instances (i.e. #respondsToSelector: with the message selector should return NO) and the undoManager must first
            know the target intended for the message (set it with #setCurrentTarget:). In our example we have:

                    - (void)setRadius:(float)aRadius
                    {
                            ....
                            // start a new undo group    
                            [undoManager startUndo:@"Undo Change Radius" :@"Redo Change Radius"];
                            
                            // set the intended target for the message in the undo process
                            [undoManager setCurrentTarget:self];
                            
                            // send the message to the undoManager. The casting is necessary otherwise
                            // you get a warning with "MiscUndoManager does not respond..."
                            [(id)undoManager setRadius:radius]; 	

                            // commit group
                            [undoManager commitUndo];
                            
                            radius = aRadius;
                            ....
                    }

	2.) Construct the NSInvocation instance yourself. In the example method we have:

                    - (void)setRadius:(float)aRadius
                    {
                            ...
                            NSInvocation *undoRecord;
                            SEL undoSelector;
                            NSMethodSignature *undoSignature;

                            ....
                            // start a new undo group    
                            [undoManager startUndo:@"Undo Change Radius" :@"Redo Change Radius"];

                            // construct undoRecord
                            undoSelector = @selector(setRadius:);
                            undoSignature = [self methodSignatureForSelector:undoSelector];
                            undoRecord = [NSInvocation invocationWithMethodSignature:undoSignature];
                            [undoRecord setSelector:undoSelector];
                            [undoRecord setTarget:self];
                            [undoRecord setArgument:&radius atIndex:2];
                            [undoRecord retainArguments];

                            // register undoRecord
                            [undoManager registerRecord:undoRecord];
			
                            // commit group
                            [undoManager commitUndo];
                            
                            radius = aRadius;
                            ....
                    }
  
   
The method #setLevelsOfUndo: sets the number of levels of undo to maintain. When the number of records exceeds this amount, the excess is removed. Before and after each undo and redo the MiscUndoManager notifies the default notification center.
"*/


- init
{
	[super init];
	disabled = NO;
	levelsOfUndo = 99;
	undoArray = [[NSMutableArray allocWithZone:[self zone]] initWithCapacity:99];
	redoArray = [[NSMutableArray allocWithZone:[self zone]] initWithCapacity:99];
	currentGroup = nil;
	undoing = redoing = NO;
        currentTarget = nil;
	return self;
}

- (void)dealloc
{
	[self emptyManager];
	[undoArray release];
	[redoArray release];
	return [super dealloc];
}

- (void)startUndo:(NSString *)undoName :(NSString *)redoName
/*"Starts a new record group with undo name undoName and redo name redoName. 
If the current group has not been commited it is released."*/
{
	if(currentGroup)
		[currentGroup release];
	currentGroup = [[MiscUndoGroup groupWithUndoName:undoName redoName:redoName] retain];
}

- (void)commitUndo
/*"Commits the current group of MiscUndoRecords to the undoArray or if 
the manager is in "redo" state to the redoArray. If no group has been 
started this method does nothing."*/
{
	if(!currentGroup)
		return;
	if(undoing)
		[redoArray addObject:currentGroup];
	else {
		if([undoArray count] >= levelsOfUndo)
			[undoArray removeObjectAtIndex:0];
		[undoArray addObject:currentGroup];
	}
	[currentGroup release];
	currentGroup = nil;
	if(!undoing && !redoing)
		[redoArray removeAllObjects];
}

- (void)registerRecord:(NSInvocation *)record
/*"Adds record to the current record group. Note the group must first be commited before the records can be undone."*/
{
	if(currentGroup && !disabled)
		[currentGroup addUndoRecord:record];
}

- (NSString *)undoName
/*"Returns the undo name of the group last commited to the undoArray. 
Can be used to set the title of the "Undo" menu cell."*/
{
	return [[undoArray lastObject] undoName];
}

- (NSString *)redoName;
/*"Returns the redo name of the group last commited to the redoArray. 
Can be used to set the title of the "Redo" menu cell."*/
{
	return [[redoArray lastObject] redoName];
}

- (unsigned)numberOfUndos
/*"Returns the number of groups in the redoArray. Can be used to establish 
whether the "Undo" menu cell should be disabled."*/
{
	return [undoArray count];
}

- (unsigned)numberOfRedos
/*"Returns the number of groups in the redoArray. Can be used to establish 
whether the "Redo" menu cell should be disabled."*/
{
	return [redoArray count];
}

- (void)undo
/*"Removes the group of MiscUndoRecords last commited to the undoArray and sends all the 
records in the group the message #dispatch. Puts the manager in the "redo" state. Notifies the 
default notification center at the start of the method with the string "MiscWillUndo" and at the 
end of the method with "MiscDidUndo"."*/
{	
	if([undoArray count] == 0)
		return;
	undoing = YES;
	[[NSNotificationCenter defaultCenter]
    	postNotificationName:@"MiscWillUndo" object:self];	
	[[undoArray lastObject] dispatch];
	[undoArray removeLastObject];
	[[NSNotificationCenter defaultCenter]
    	postNotificationName:@"MiscDidUndo" object:self];
	undoing = NO;
}

- (void)redo
/*"Removes the group of MiscUndoRecords last commited to the redoArray and sends all the records 
in the group the message #dispatch. Puts the manager in the "undo" state. Notifies the default notification 
center at the start of the method with the string "MiscWillRedo" and at the end of the method with "MiscDidRedo"."*/
{	
	if([redoArray count] == 0)
		return;
	redoing = YES;
	[[NSNotificationCenter defaultCenter]
    	postNotificationName:@"MiscWillRedo" object:self];
	[[redoArray lastObject] dispatch];
	[redoArray removeLastObject];
	[[NSNotificationCenter defaultCenter]
    	postNotificationName:@"MiscDidRedo" object:self];
	redoing = NO;
}

- (void)disableUndoRegistration
/*"Disables the registration of records. Should be used in pair with #reenableUndoRegistration. Can be nested."*/
{
	disabled++;
}

- (void)reenableUndoRegistration
/*"Reenables the registration of records. The manager is only then reenabled for registration when for every nested #disableUndoRegistration message the manager has received a #reenableUndoRegistration message."*/
{	
	disabled--;
}

- (unsigned)levelsOfUndo
/*"Returns the number of levels of undo the manager maintains."*/
{
	return levelsOfUndo;
}

- (void)setLevelsOfUndo:(unsigned)value
/*"Sets the number of undo levels maintained by the manager to value. The default number of levels is 99."*/
{
	levelsOfUndo = value;
}

- (void)emptyManager
/*"Resets the manager. The current record group is released, the undoArray and the 
redoArray are emptied. Appropiate for sending for example to an MiscUndoManager of a 
document when the document is saved."*/
{
	if(currentGroup){
		[currentGroup release];
		currentGroup = nil;
	}
	[undoArray removeAllObjects];
	[redoArray removeAllObjects];
	return;
}

- (void)setCurrentTarget:aTarget
/*"Sets the intended target for the message to be executed in an undo process. Does not retain aTarget.
You must make sure that aTarget is still valid when you send a message to be registered (see example)."*/
{
    	currentTarget = aTarget;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
        if(!currentGroup || !currentTarget){
            	[self doesNotRecognizeSelector:[anInvocation selector]];
        	return;
        }                
        [anInvocation setTarget:currentTarget];
        [anInvocation retainArguments];
        [self registerRecord:anInvocation];
        currentTarget = nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
	if([self respondsToSelector:aSelector])
		return [super methodSignatureForSelector:aSelector];
	else if(currentTarget && [currentTarget respondsToSelector:aSelector])
		return [currentTarget methodSignatureForSelector:aSelector];
	else 
		return nil;
}

@end


