1/* ChangeManager.m 2 * 3 * Copyright (C) 1993-2006 by vhf interservice GmbH 4 * Authors: Georg Fleischmann 5 * 6 * created: 1993 based on the Draw example files 7 * modified: 2006-11-07 8 * 9 * This program is free software; you can redistribute it and/or 10 * modify it under the terms of the vhf Public License as 11 * published by vhf interservice GmbH. Among other things, the 12 * License requires that the copyright notices and this notice 13 * be preserved on all copies. 14 * 15 * This program is distributed in the hope that it will be useful, 16 * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 18 * See the vhf Public License for more details. 19 * 20 * You should have received a copy of the vhf Public License along 21 * with this program; see the file LICENSE. If not, write to vhf. 22 * 23 * vhf interservice GmbH, Im Marxle 3, 72119 Altingen, Germany 24 * eMail: info@vhf.de 25 * http://www.vhf.de 26 */ 27 28#include "undochange.h" 29 30/* N_LEVEL_UNDO sets the maximum number of changes that the ChangeManager 31 * will keep track of. Set this to 1 to get single level undo behaviour. 32 * Set it to a really big number if you want to offer nearly infinite undo. 33 * Be careful if you do this because unless you explicity reset the 34 * ChangeManager from time to time, like whenever you save a document, the 35 * ChangeManager will never forget changes and will eventually chew up 36 * enourmous amounts of swapfile. 37 */ 38#define N_LEVEL_UNDO 25 39 40@interface ChangeManager(PrivateMethods) 41 42- (void)updateMenuItem:(NSMenuItem*) menuItem with:(NSString *)menuText; 43 44@end 45 46@implementation ChangeManager 47 48/* Methods called directly by your code */ 49 50- (id)init 51{ 52 [super init]; 53 54 _changeList = [[NSMutableArray alloc] initWithCapacity:N_LEVEL_UNDO]; 55 _numberOfDoneChanges = 0; 56 _numberOfUndoneChanges = 0; 57 _numberOfDoneChangesAtLastClean = 0; 58 _someChangesForgotten = NO; 59 _lastChange = nil; 60 _nextChange = nil; 61 _changeInProgress = nil; 62 _changesDisabled = 0; 63 64 return self; 65} 66 67- (void)dealloc 68{ 69 [self reset:self]; 70 [_changeList release]; 71 [super dealloc]; 72} 73 74- (BOOL)canUndo 75{ 76 if (_lastChange == nil) 77 return NO; 78 NSAssert1(![_lastChange changeInProgress], @"%@", "Fault in Undo system: Code 1"); 79 NSAssert1(![_lastChange disabled], @"%@", "Fault in Undo system: Code 2"); 80 NSAssert1([_lastChange hasBeenDone], @"%@", "Fault in Undo system: Code 3"); 81 return YES; 82} 83 84- (BOOL)canRedo 85{ 86 if (_nextChange == nil) 87 return NO; 88 NSAssert1(![_nextChange changeInProgress], @"%@", "Fault in Undo system: Code 4"); 89 NSAssert1(![_nextChange disabled], @"%@", "Fault in Undo system: Code 5"); 90 NSAssert1(![_nextChange hasBeenDone], @"%@", "Fault in Undo system: Code 6"); 91 return YES; 92} 93 94- (BOOL)isDirty 95{ 96 return ((_numberOfDoneChanges != _numberOfDoneChangesAtLastClean) 97 || _someChangesForgotten); 98} 99 100- (void)dirty:sender 101{ 102 _someChangesForgotten = YES; 103} 104 105- (void)clean:sender 106{ 107 _someChangesForgotten = NO; 108 _numberOfDoneChangesAtLastClean = _numberOfDoneChanges; 109} 110 111- (void)reset:sender 112{ 113 [_changeList removeAllObjects]; 114 _numberOfDoneChanges = 0; 115 _numberOfUndoneChanges = 0; 116 _numberOfDoneChangesAtLastClean = 0; 117 _someChangesForgotten = NO; 118 _lastChange = nil; 119 _nextChange = nil; 120 _changeInProgress = nil; 121 _changesDisabled = 0; 122} 123 124/* 125 * disableChanges: and enableChanges: work as a team, incrementing and 126 * decrementing the _changesDisabled count. We use a count instead of 127 * a BOOL so that nested disables will work correctly -- the outermost 128 * disable and enable pair are the only ones that do anything. 129 */ 130- (void)disableChanges:sender 131{ 132 _changesDisabled++; 133} 134 135/* 136 * We're forgiving if we get an enableChanges: that doesn't match up 137 * with any previous disableChanges: call. 138 */ 139- (void)enableChanges:sender 140{ 141 if (_changesDisabled > 0) 142 _changesDisabled--; 143} 144 145- (void)undoOrRedoChange:sender 146{ 147 if ([self canUndo]) 148 [self undoChange:sender]; 149 if ([self canRedo]) 150 [self redoChange:sender]; 151} 152 153- (void)undoChange:sender 154{ 155 if ([self canUndo]) 156 { 157 [_lastChange finishChange]; 158 [self disableChanges:self]; 159 [_lastChange undoChange]; 160 [self enableChanges:self]; 161 _nextChange = _lastChange; 162 _lastChange = nil; 163 _numberOfDoneChanges--; 164 _numberOfUndoneChanges++; 165 if (_numberOfDoneChanges > 0) 166 _lastChange = [_changeList objectAtIndex:(_numberOfDoneChanges - 1)]; 167 [self changeWasUndone]; 168 } 169} 170 171- (void)redoChange:sender 172{ 173 if ([self canRedo]) 174 { 175 [self disableChanges:self]; 176 [_nextChange redoChange]; 177 [self enableChanges:self]; 178 _lastChange = _nextChange; 179 _nextChange = nil; 180 _numberOfDoneChanges++; 181 _numberOfUndoneChanges--; 182 if (_numberOfUndoneChanges > 0) 183 _nextChange = [_changeList objectAtIndex:_numberOfDoneChanges]; 184 [self changeWasRedone]; 185 } 186} 187 188/* 189 */ 190- (BOOL)validateMenuItem:(NSMenuItem*)anItem 191{ SEL action; 192 BOOL canUndo, canRedo, enableMenuItem = YES; 193 NSString *menuText; 194 195 action = [anItem action]; 196 197 if (action == @selector(undoOrRedoChange:)) 198 { 199 enableMenuItem = NO; 200 canUndo = [self canUndo]; 201 if (canUndo) 202 { menuText = [NSString stringWithFormat:UNDO_SOMETHING_OPERATION, [_lastChange changeName]]; 203 enableMenuItem = YES; 204 } 205 else 206 { 207 canRedo = [self canRedo]; 208 if (canRedo) 209 { menuText = [NSString stringWithFormat:REDO_SOMETHING_OPERATION, [_nextChange changeName]]; 210 enableMenuItem = YES; 211 } 212 else 213 menuText = [NSString stringWithFormat:UNDO_OPERATION]; 214 } 215 [self updateMenuItem:anItem with:menuText]; 216 } 217 218 if (action == @selector(undoChange:)) 219 { 220 canUndo = [self canUndo]; 221 if (!canUndo) 222 menuText = [NSString stringWithFormat:UNDO_OPERATION]; 223 else 224 menuText = [NSString stringWithFormat:UNDO_SOMETHING_OPERATION, [_lastChange changeName]]; 225 [self updateMenuItem:anItem with:menuText]; 226 enableMenuItem = canUndo; 227 } 228 229 if (action == @selector(redoChange:)) 230 { 231 canRedo = [self canRedo]; 232 if (!canRedo) 233 menuText = [NSString stringWithFormat:REDO_OPERATION]; 234 else 235 menuText = [NSString stringWithFormat:REDO_SOMETHING_OPERATION, [_nextChange changeName]]; 236 [self updateMenuItem:anItem with:menuText]; 237 enableMenuItem = canRedo; 238 } 239 240 return enableMenuItem; 241} 242 243/* Methods called by Change 244 * DO NOT call these methods directly 245 */ 246 247/* 248 * The changeInProgress: and changeComplete: methods are the most 249 * complicated part of the undo framework. Their behaviour is documented 250 * in the external reference sheets. 251 */ 252- (BOOL)changeInProgress:change 253{ 254 if (_changesDisabled > 0) 255 { 256 [change disable]; 257 return NO; 258 } 259 260 if (_changeInProgress != nil) 261 { 262 if ([_changeInProgress incorporateChange:change]) 263 { 264 /* The _changeInProgress will keep a pointer to this 265 * change and make use of it, but we have no further 266 * responsibility for it. 267 */ 268 [change saveBeforeChange]; 269 return YES; 270 } 271 else 272 { 273 /* The _changeInProgress has no more interest in this 274 * change than we do, so we'll just disable it. 275 */ 276 [change disable]; 277 return NO; 278 } 279 } 280 281 if (_lastChange != nil) 282 { 283 if ([_lastChange subsumeChange:change]) 284 { 285 /* The _lastChange has subsumed this change and 286 * may either make use of it or free it, but we 287 * have no further responsibility for it. 288 */ 289 [change disable]; 290 return NO; 291 } 292 else 293 { 294 /* The _lastChange was not able to subsume this change, 295 * so we give the _lastChange a chance to finish and then 296 * welcome this change as the new _changeInProgress. 297 */ 298 [_lastChange finishChange]; 299 } 300 } 301 302 /* 303 * This will be a new, independent change. 304 */ 305 [change saveBeforeChange]; 306 if (![change disabled]) 307 _changeInProgress = change; 308 309 return YES; 310} 311 312/* 313 * The changeInProgress: and changeComplete: methods are the most 314 * complicated part of the undo framework. Their behaviour is documented 315 * in the external reference sheets. 316 */ 317- (BOOL)changeComplete:change 318{ int i; 319 320 NSAssert1(![change changeInProgress], @"%@", "Fault in Undo system: Code 7"); 321 NSAssert1(![change disabled], @"%@", "Fault in Undo system: Code 8"); 322 NSAssert1([change hasBeenDone], @"%@", "Fault in Undo system: Code 9"); 323 if (change != _changeInProgress) 324 { 325 /* Actually, we come here whenever a change is 326 * incorportated or subsumed by another change 327 * and later executes its endChange method. 328 */ 329 [change saveAfterChange]; 330 return NO; 331 } 332 333 if (_numberOfUndoneChanges > 0) 334 { 335 NSAssert1(_numberOfDoneChanges != N_LEVEL_UNDO, @"%@", "Fault in Undo system: Code 10"); 336 /* Remove and free all undone changes */ 337 for (i = (_numberOfDoneChanges + _numberOfUndoneChanges); i > _numberOfDoneChanges; i--) 338 [_changeList removeObjectAtIndex:(i - 1)]; 339 _nextChange = nil; 340 _numberOfUndoneChanges = 0; 341 if (_numberOfDoneChanges < _numberOfDoneChangesAtLastClean) 342 _someChangesForgotten = YES; 343 } 344 if (_numberOfDoneChanges == N_LEVEL_UNDO) 345 { 346 NSAssert1(_numberOfUndoneChanges == 0, @"%@", "Fault in Undo system: Code 11"); 347 NSAssert1(_nextChange == nil, @"%@", "Fault in Undo system: Code 12"); 348 [_changeList removeObjectAtIndex:0]; 349 _numberOfDoneChanges--; 350 _someChangesForgotten = YES; 351 } 352 [_changeList addObject:change]; 353 _numberOfDoneChanges++; 354 355 _lastChange = change; 356 _changeInProgress = nil; 357 358 [change saveAfterChange]; 359 [self changeWasDone]; 360 361 return YES; 362} 363 364/* Methods called by ChangeManager */ 365/* DO NOT call these methods directly */ 366 367/* To be overridden 368 */ 369- (void)changeWasDone 370{ 371} 372 373/* To be overridden 374 */ 375- (void)changeWasUndone 376{ 377} 378 379/* To be overridden 380 */ 381- (void)changeWasRedone 382{ 383} 384 385/* Private Methods 386 */ 387 388- (void)updateMenuItem:(NSMenuItem*)menuItem with:(NSString *)menuText 389{ 390 [menuItem setTitleWithMnemonic:menuText]; 391} 392 393@end 394