home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
OpenStep (Enterprise)
/
OpenStepENTCD.toast
/
OEDEV
/
DEV.Z
/
ChangeManager.m
< prev
next >
Wrap
Text File
|
1996-08-13
|
10KB
|
393 lines
#import "undochange.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
@interface ChangeManager(PrivateMethods)
- (void)updateMenuItem:(id <NSMenuItem>) menuItem with:(NSString *)menuText;
@end
@implementation ChangeManager
/* Methods called directly by your code */
- (id)init
{
[super init];
_changeList = [[NSMutableArray alloc] initWithCapacity:N_LEVEL_UNDO];
_numberOfDoneChanges = 0;
_numberOfUndoneChanges = 0;
_numberOfDoneChangesAtLastClean = 0;
_someChangesForgotten = NO;
_lastChange = nil;
_nextChange = nil;
_changeInProgress = nil;
_changesDisabled = 0;
return self;
}
- (void)dealloc
{
[self reset:self];
[_changeList release];
[super dealloc];
}
- (BOOL)canUndo
{
if (_lastChange == nil) {
return NO;
} else {
NSAssert1(![_lastChange changeInProgress], @"%@", "Fault in Undo system: Code 1");
NSAssert1(![_lastChange disabled], @"%@", "Fault in Undo system: Code 2");
NSAssert1([_lastChange hasBeenDone], @"%@", "Fault in Undo system: Code 3");
return YES;
}
}
- (BOOL)canRedo
{
if (_nextChange == nil) {
return NO;
} else {
NSAssert1(![_nextChange changeInProgress], @"%@", "Fault in Undo system: Code 4");
NSAssert1(![_nextChange disabled], @"%@", "Fault in Undo system: Code 5");
NSAssert1(![_nextChange hasBeenDone], @"%@", "Fault in Undo system: Code 6");
return YES;
}
}
- (BOOL)isDirty
{
return ((_numberOfDoneChanges != _numberOfDoneChangesAtLastClean)
|| _someChangesForgotten);
}
- (void)dirty:sender
{
_someChangesForgotten = YES;
}
- (void)clean:sender
{
_someChangesForgotten = NO;
_numberOfDoneChangesAtLastClean = _numberOfDoneChanges;
}
- (void)reset:sender
{
[_changeList removeAllObjects];
_numberOfDoneChanges = 0;
_numberOfUndoneChanges = 0;
_numberOfDoneChangesAtLastClean = 0;
_someChangesForgotten = NO;
_lastChange = nil;
_nextChange = nil;
_changeInProgress = nil;
_changesDisabled = 0;
}
- (void)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++;
}
- (void)enableChanges:sender
/*
* We're forgiving if we get an enableChanges: that doesn't match up
* with any previous disableChanges: call.
*/
{
if (_changesDisabled > 0)
_changesDisabled--;
}
- (void)undoOrRedoChange:sender
{
if ([self canUndo]) {
[self undoChange:sender];
} else {
if ([self canRedo]) {
[self redoChange:sender];
}
}
}
- (void)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 objectAtIndex:(_numberOfDoneChanges - 1)];
}
[self changeWasUndone];
}
}
- (void)redoChange:sender
{
if ([self canRedo]) {
[self disableChanges:self];
[_nextChange redoChange];
[self enableChanges:self];
_lastChange = _nextChange;
_nextChange = nil;
_numberOfDoneChanges++;
_numberOfUndoneChanges--;
if (_numberOfUndoneChanges > 0) {
_nextChange = [_changeList objectAtIndex:_numberOfDoneChanges];
}
[self changeWasRedone];
}
}
- (BOOL)validateMenuItem:(id <NSMenuItem>)anItem
/*
* See the Draw code for a good example of how validateCell:
* can be used to keep the application's menu items up to date.
*/
{
SEL action;
BOOL canUndo, canRedo, enableMenuItem = YES;
NSString *menuText;
action = [anItem action];
if (action == @selector(undoOrRedoChange:)) {
enableMenuItem = NO;
canUndo = [self canUndo];
if (canUndo) {
menuText = [NSString stringWithFormat:UNDO_SOMETHING_OPERATION, [_lastChange changeName]];
enableMenuItem = YES;
} else {
canRedo = [self canRedo];
if (canRedo) {
menuText = [NSString stringWithFormat:REDO_SOMETHING_OPERATION, [_nextChange changeName]];
enableMenuItem = YES;
} else {
menuText = [NSString stringWithFormat:UNDO_OPERATION];
}
}
[self updateMenuItem:anItem with:menuText];
}
if (action == @selector(undoChange:)) {
canUndo = [self canUndo];
if (!canUndo) {
menuText = [NSString stringWithFormat:UNDO_OPERATION];
} else {
menuText = [NSString stringWithFormat:UNDO_SOMETHING_OPERATION, [_lastChange changeName]];
}
[self updateMenuItem:anItem with:menuText];
enableMenuItem = canUndo;
}
if (action == @selector(redoChange:)) {
canRedo = [self canRedo];
if (!canRedo) {
menuText = [NSString stringWithFormat:REDO_OPERATION];
} else {
menuText = [NSString stringWithFormat:REDO_SOMETHING_OPERATION, [_nextChange changeName]];
}
[self updateMenuItem:anItem with:menuText];
enableMenuItem = canRedo;
}
return enableMenuItem;
}
/* Methods called by Change */
/* DO NOT call these methods directly */
- (BOOL)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 NO;
}
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 YES;
} else {
/*
* The _changeInProgress has no more interest in this
* change than we do, so we'll just disable it.
*/
[change disable];
return NO;
}
}
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 NO;
} 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 YES;
}
- (BOOL)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;
NSAssert1(![change changeInProgress], @"%@", "Fault in Undo system: Code 7");
NSAssert1(![change disabled], @"%@", "Fault in Undo system: Code 8");
NSAssert1([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 NO;
}
if (_numberOfUndoneChanges > 0) {
NSAssert1(_numberOfDoneChanges != N_LEVEL_UNDO, @"%@", "Fault in Undo system: Code 10");
/* Remove and free all undone changes */
for (i = (_numberOfDoneChanges + _numberOfUndoneChanges); i > _numberOfDoneChanges; i--) {
[_changeList removeObjectAtIndex:(i - 1)];
}
_nextChange = nil;
_numberOfUndoneChanges = 0;
if (_numberOfDoneChanges < _numberOfDoneChangesAtLastClean)
_someChangesForgotten = YES;
}
if (_numberOfDoneChanges == N_LEVEL_UNDO) {
NSAssert1(_numberOfUndoneChanges == 0, @"%@", "Fault in Undo system: Code 11");
NSAssert1(_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 removeObjectAtIndex:0];
_numberOfDoneChanges--;
_someChangesForgotten = YES;
}
[_changeList addObject:change];
_numberOfDoneChanges++;
_lastChange = change;
_changeInProgress = nil;
[change saveAfterChange];
[self changeWasDone];
return YES;
}
/* Methods called by ChangeManager */
/* DO NOT call these methods directly */
- (void)changeWasDone
/*
* To be overridden
*/
{
}
- (void)changeWasUndone
/*
* To be overridden
*/
{
}
- (void)changeWasRedone
/*
* To be overridden
*/
{
}
/* Private Methods */
- (void)updateMenuItem:(id <NSMenuItem>)menuItem with:(NSString *)menuText
{
[menuItem setTitleWithMnemonic:menuText];
}
@end