ftp.nice.ch/pub/next/science/mathematics/HippoDraw.2.0.s.tar.gz#/HippoDraw/Hippo.bproj/Draw.subproj/undo.subproj/ChangeManager.m

This is ChangeManager.m in view mode; [Download] [Up]

#import "change.h"

/*
 * Please refer to external reference pages for complete
 * documentation on using the ChangeManager class.
 */

/* 
 * N_LEVEL_UNDO sets the maximum number of changes that the ChangeManager
 * will keep track of. Set this to 1 to get single level undo behaviour. 
 * Set it to a really big number if you want to offer nearly infinite undo.
 * Be careful if you do this because unless you explicity reset the 
 * ChangeManager from time to time, like whenever you save a document, the
 * ChangeManager will never forget changes and will eventually chew up
 * enourmous amounts of swapfile.
 */
#define	N_LEVEL_UNDO	10

/* Localization */

#define UNDO_OPERATION NXLocalStringFromTable("Operations", "Undo", NULL, "The operation of undoing the last thing the user did.")
#define UNDO_SOMETHING_OPERATION NXLocalStringFromTable("Operations", "Undo %s", NULL, "The operation of undoing the last %s operation the user did--all the entries in the Operations and TextOperations .strings files are the %s of this or Redo.")
#define REDO_OPERATION NXLocalStringFromTable("Operations", "Redo", NULL, "The operation of redoing the last thing the user undid.")
#define REDO_SOMETHING_OPERATION NXLocalStringFromTable("Operations", "Redo %s", NULL, "The operation of redoing the last %s operation the user undid--all the entries in the Operations and TextOperations .strings files are the %s of either this or Undo.")

@interface ChangeManager(PrivateMethods)

- updateMenuCell:menuCell with:(const char *)menuText;

@end

@implementation ChangeManager
/* Methods called directly by your code */

- init
{
    [super init];

    _changeList = [[List alloc] initCount:N_LEVEL_UNDO];
    _numberOfDoneChanges = 0;
    _numberOfUndoneChanges = 0;
    _numberOfDoneChangesAtLastClean = 0;
    _someChangesForgotten = NO;
    _lastChange = nil;
    _nextChange = nil;
    _changeInProgress = nil;
    _changesDisabled = 0;
    
    return self;
}

- free
{
    [self reset:self];
    [_changeList free];
    return [super free];
}

- (BOOL)canUndo
{
    if (_lastChange == nil) {
        return NO;
    } else {
	NX_ASSERT(![_lastChange changeInProgress], "Fault in Undo system: Code 1");
	NX_ASSERT(![_lastChange disabled], "Fault in Undo system: Code 2");
	NX_ASSERT([_lastChange hasBeenDone], "Fault in Undo system: Code 3");
        return YES;
    }
}

- (BOOL)canRedo
{
    if (_nextChange == nil) {
        return NO;
    } else {
	NX_ASSERT(![_nextChange changeInProgress], "Fault in Undo system: Code 4");
	NX_ASSERT(![_nextChange disabled], "Fault in Undo system: Code 5");
	NX_ASSERT(![_nextChange hasBeenDone], "Fault in Undo system: Code 6");
        return YES;
    }
}

- (BOOL)isDirty
{
    return ((_numberOfDoneChanges != _numberOfDoneChangesAtLastClean) 
    		|| _someChangesForgotten);
}

- dirty:sender
{
    _someChangesForgotten = YES;
    return self;
}

- clean:sender
{
    _someChangesForgotten = NO;
    _numberOfDoneChangesAtLastClean = _numberOfDoneChanges;
    return self;
}

- reset:sender
{
    while(_lastChange = [_changeList removeLastObject]) {
	[_lastChange free];
    }
    _numberOfDoneChanges = 0;
    _numberOfUndoneChanges = 0;
    _numberOfDoneChangesAtLastClean = 0;
    _someChangesForgotten = NO;
    _lastChange = nil;
    _nextChange = nil;
    _changeInProgress = nil;
    _changesDisabled = 0;

    return self;
}

- disableChanges:sender
/*
 * disableChanges: and enableChanges: work as a team, incrementing and
 * decrementing the _changesDisabled count. We use a count instead of
 * a BOOL so that nested disables will work correctly -- the outermost
 * disable and enable pair are the only ones that do anything.
 */
{
    _changesDisabled++;
    return self;
}

- enableChanges:sender
/*
 * We're forgiving if we get an enableChanges: that doesn't match up
 * with any previous disableChanges: call.
 */
{
    if (_changesDisabled > 0)
        _changesDisabled--;
    return self;
}

- undoOrRedoChange:sender
{
    if ([self canUndo]) {
        [self undoChange:sender];
    } else {
	if ([self canRedo]) {
	    [self redoChange:sender];
	}
    }
    return self;
}

- undoChange:sender
{
    if ([self canUndo]) {
	[_lastChange finishChange];
	[self disableChanges:self];
	    [_lastChange undoChange];
	[self enableChanges:self];
	_nextChange = _lastChange;
	_lastChange = nil;
	_numberOfDoneChanges--;
	_numberOfUndoneChanges++;
	if (_numberOfDoneChanges > 0) {
	    _lastChange = [_changeList objectAt:(_numberOfDoneChanges - 1)];
	}
	[self changeWasUndone];
    }
    
    return self;
}

- redoChange:sender
{
    if ([self canRedo]) {
	[self disableChanges:self];
	    [_nextChange redoChange];
	[self enableChanges:self];
	_lastChange = _nextChange;
	_nextChange = nil;
	_numberOfDoneChanges++;
	_numberOfUndoneChanges--;
	if (_numberOfUndoneChanges > 0) {
	    _nextChange = [_changeList objectAt:_numberOfDoneChanges];
	}
	[self changeWasRedone];
    }
    
    return self;
}

- (BOOL)validateCommand:menuCell
/*
 * See the Draw code for a good example of how validateCommand:
 * can be used to keep the application's menu items up to date.
 */
{
    SEL action;
    BOOL canUndo, canRedo, enableMenuItem = YES;
    char menuText[256];

    action = [menuCell action];
    
    if (action == @selector(undoOrRedoChange:)) {
        enableMenuItem = NO;
	canUndo = [self canUndo];
	if (canUndo) {
	    sprintf(menuText, UNDO_SOMETHING_OPERATION, [_lastChange changeName]);
	    enableMenuItem = YES;
	} else {
	    canRedo = [self canRedo];
	    if (canRedo) {
	        sprintf(menuText, REDO_SOMETHING_OPERATION, [_nextChange changeName]);
	        enableMenuItem = YES;
	    } else {
		sprintf(menuText, UNDO_OPERATION);
	    }
	}
	[self updateMenuCell:menuCell with:menuText];
	enableMenuItem = NO;
    }

    if (action == @selector(undoChange:)) {
	canUndo = [self canUndo];
	if (!canUndo) {
	    sprintf(menuText, UNDO_OPERATION);
	} else {
	    sprintf(menuText, UNDO_SOMETHING_OPERATION, [_lastChange changeName]);
	}
	[self updateMenuCell:menuCell with:menuText];
	enableMenuItem = NO;
    }

    if (action == @selector(redoChange:)) {
	canRedo = [self canRedo];
	if (!canRedo) {
	    sprintf(menuText, REDO_OPERATION);
	} else {
	    sprintf(menuText, REDO_SOMETHING_OPERATION, [_nextChange changeName]);
	}
	[self updateMenuCell:menuCell with:menuText];
	enableMenuItem = NO;
    }

    return enableMenuItem;
}

/* Methods called by Change           */
/* DO NOT call these methods directly */

- changeInProgress:change
/*
 * The changeInProgress: and changeComplete: methods are the most
 * complicated part of the undo framework. Their behaviour is documented 
 * in the external reference sheets.
 */
{
    if (_changesDisabled > 0) {
	[change disable];
	return nil;
    } 
    
    if (_changeInProgress != nil) {
	if ([_changeInProgress incorporateChange:change]) {
	    /* 
	     * The _changeInProgress will keep a pointer to this
	     * change and make use of it, but we have no further
	     * responsibility for it.
	     */
	    [change saveBeforeChange];
	    return self;
	} else {
	    /* 
	     * The _changeInProgress has no more interest in this
	     * change than we do, so we'll just disable it.
	     */
	    [change disable];
	    return nil;
	}
    } 
    
    if (_lastChange != nil) {
	if ([_lastChange subsumeChange:change]) {
	    /* 
	     * The _lastChange has subsumed this change and 
	     * may either make use of it or free it, but we
	     * have no further responsibility for it.
	     */
	    [change disable];
	    return nil;
	} else {
	    /* 
	     * The _lastChange was not able to subsume this change, 
	     * so we give the _lastChange a chance to finish and then
	     * welcome this change as the new _changeInProgress.
	     */
	    [_lastChange finishChange];
        }
    }

    /* 
     * This will be a new, independent change.
     */
    [change saveBeforeChange];
    if (![change disabled])
        _changeInProgress = change;
    return self;
}

- changeComplete:change
/*
 * The changeInProgress: and changeComplete: methods are the most
 * complicated part of the undo framework. Their behaviour is documented 
 * in the external reference sheets.
 */
{
    int i;
    
    NX_ASSERT(![change changeInProgress], "Fault in Undo system: Code 7");
    NX_ASSERT(![change disabled], "Fault in Undo system: Code 8");
    NX_ASSERT([change hasBeenDone], "Fault in Undo system: Code 9");
    if (change != _changeInProgress) {
	/* 
	 * "Toto, I don't think we're in Kansas anymore."
	 *				- Dorthy
	 * Actually, we come here whenever a change is 
	 * incorportated or subsumed by another change 
	 * and later executes its endChange method.
	 */
        [change saveAfterChange];
	return nil;
    }
    
    if (_numberOfUndoneChanges > 0) {
	NX_ASSERT(_numberOfDoneChanges != N_LEVEL_UNDO, "Fault in Undo system: Code 10");
	/* Remove and free all undone changes */
	for (i = (_numberOfDoneChanges + _numberOfUndoneChanges); i > _numberOfDoneChanges; i--) {
	    [[_changeList removeObjectAt:(i - 1)] free];
	}
	_nextChange = nil;
	_numberOfUndoneChanges = 0;
	if (_numberOfDoneChanges < _numberOfDoneChangesAtLastClean)
	    _someChangesForgotten = YES;
    }
    if (_numberOfDoneChanges == N_LEVEL_UNDO) {
	NX_ASSERT(_numberOfUndoneChanges == 0, "Fault in Undo system: Code 11");
	NX_ASSERT(_nextChange == nil, "Fault in Undo system: Code 12");
	/* 
	    * The [_changeList removeObjectAt:0] call is order N.
	    * This will be slow if N_LEVEL_UNDO is large.
	    * Ideally the _changeList should be implemented as
	    * a circular queue, or List should do removeObjectAt:
	    * in a fixed time. In many applications (including
	    * Draw) doing the redisplay associated with the undo 
	    * will take MUCH longer than even an order N call to 
	    * removeObjectAt:, so it's not too important that 
	    * this be changed.
	    */
	[[_changeList removeObjectAt:0] free];
	_numberOfDoneChanges--;
	_someChangesForgotten = YES;
    }
    [_changeList addObject:change];
    _numberOfDoneChanges++;

    _lastChange = change;
    _changeInProgress = nil;

    [change saveAfterChange];
    [self changeWasDone];

    return self;
}

/* Methods called by ChangeManager    */
/* DO NOT call these methods directly */

- changeWasDone
/*
 * To be overridden 
 */
{
    return self;
}

- changeWasUndone
/*
 * To be overridden 
 */
{
    return self;
}

- changeWasRedone
/*
 * To be overridden 
 */
{
    return self;
}

/* Private Methods    */

- updateMenuCell:menuCell with:(const char *)menuText
{
    id editMenu, cv;

    if (strcmp([menuCell title], menuText)) {
	cv = [menuCell controlView];
	editMenu = [cv window];
#ifdef SIZE_MENU_TO_FIT
	[editMenu disableDisplay];
	    [menuCell setTitle:menuText];
	    [editMenu sizeToFit];
	[editMenu reenableDisplay];
	[editMenu display];
#else
    	if (![editMenu isDisplayEnabled]) [editMenu reenableDisplay];
    	[menuCell setTitle:menuText];
#endif
    }
    return self;
}

@end

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.