|
|
1.1 ! root 1: #import "change.h" ! 2: ! 3: /* ! 4: * Please refer to external reference pages for complete ! 5: * documentation on using the ChangeManager class. ! 6: */ ! 7: ! 8: /* ! 9: * N_LEVEL_UNDO sets the maximum number of changes that the ChangeManager ! 10: * will keep track of. Set this to 1 to get single level undo behaviour. ! 11: * Set it to a really big number if you want to offer nearly infinite undo. ! 12: * Be careful if you do this because unless you explicity reset the ! 13: * ChangeManager from time to time, like whenever you save a document, the ! 14: * ChangeManager will never forget changes and will eventually chew up ! 15: * enourmous amounts of swapfile. ! 16: */ ! 17: #define N_LEVEL_UNDO 10 ! 18: ! 19: /* Localization */ ! 20: ! 21: #define UNDO_OPERATION NXLocalStringFromTable("Operations", "Undo", NULL, "The operation of undoing the last thing the user did.") ! 22: #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.") ! 23: #define REDO_OPERATION NXLocalStringFromTable("Operations", "Redo", NULL, "The operation of redoing the last thing the user undid.") ! 24: #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.") ! 25: ! 26: @interface ChangeManager(PrivateMethods) ! 27: ! 28: - updateMenuCell:menuCell with:(const char *)menuText; ! 29: ! 30: @end ! 31: ! 32: @implementation ChangeManager ! 33: /* Methods called directly by your code */ ! 34: ! 35: - init ! 36: { ! 37: [super init]; ! 38: ! 39: _changeList = [[List alloc] initCount:N_LEVEL_UNDO]; ! 40: _numberOfDoneChanges = 0; ! 41: _numberOfUndoneChanges = 0; ! 42: _numberOfDoneChangesAtLastClean = 0; ! 43: _someChangesForgotten = NO; ! 44: _lastChange = nil; ! 45: _nextChange = nil; ! 46: _changeInProgress = nil; ! 47: _changesDisabled = 0; ! 48: ! 49: return self; ! 50: } ! 51: ! 52: - free ! 53: { ! 54: [self reset:self]; ! 55: [_changeList free]; ! 56: return [super free]; ! 57: } ! 58: ! 59: - (BOOL)canUndo ! 60: { ! 61: if (_lastChange == nil) { ! 62: return NO; ! 63: } else { ! 64: NX_ASSERT(![_lastChange changeInProgress], "Fault in Undo system: Code 1"); ! 65: NX_ASSERT(![_lastChange disabled], "Fault in Undo system: Code 2"); ! 66: NX_ASSERT([_lastChange hasBeenDone], "Fault in Undo system: Code 3"); ! 67: return YES; ! 68: } ! 69: } ! 70: ! 71: - (BOOL)canRedo ! 72: { ! 73: if (_nextChange == nil) { ! 74: return NO; ! 75: } else { ! 76: NX_ASSERT(![_nextChange changeInProgress], "Fault in Undo system: Code 4"); ! 77: NX_ASSERT(![_nextChange disabled], "Fault in Undo system: Code 5"); ! 78: NX_ASSERT(![_nextChange hasBeenDone], "Fault in Undo system: Code 6"); ! 79: return YES; ! 80: } ! 81: } ! 82: ! 83: - (BOOL)isDirty ! 84: { ! 85: return ((_numberOfDoneChanges != _numberOfDoneChangesAtLastClean) ! 86: || _someChangesForgotten); ! 87: } ! 88: ! 89: - dirty:sender ! 90: { ! 91: _someChangesForgotten = YES; ! 92: return self; ! 93: } ! 94: ! 95: - clean:sender ! 96: { ! 97: _someChangesForgotten = NO; ! 98: _numberOfDoneChangesAtLastClean = _numberOfDoneChanges; ! 99: return self; ! 100: } ! 101: ! 102: - reset:sender ! 103: { ! 104: while(_lastChange = [_changeList removeLastObject]) { ! 105: [_lastChange free]; ! 106: } ! 107: _numberOfDoneChanges = 0; ! 108: _numberOfUndoneChanges = 0; ! 109: _numberOfDoneChangesAtLastClean = 0; ! 110: _someChangesForgotten = NO; ! 111: _lastChange = nil; ! 112: _nextChange = nil; ! 113: _changeInProgress = nil; ! 114: _changesDisabled = 0; ! 115: ! 116: return self; ! 117: } ! 118: ! 119: - disableChanges:sender ! 120: /* ! 121: * disableChanges: and enableChanges: work as a team, incrementing and ! 122: * decrementing the _changesDisabled count. We use a count instead of ! 123: * a BOOL so that nested disables will work correctly -- the outermost ! 124: * disable and enable pair are the only ones that do anything. ! 125: */ ! 126: { ! 127: _changesDisabled++; ! 128: return self; ! 129: } ! 130: ! 131: - enableChanges:sender ! 132: /* ! 133: * We're forgiving if we get an enableChanges: that doesn't match up ! 134: * with any previous disableChanges: call. ! 135: */ ! 136: { ! 137: if (_changesDisabled > 0) ! 138: _changesDisabled--; ! 139: return self; ! 140: } ! 141: ! 142: - undoOrRedoChange:sender ! 143: { ! 144: if ([self canUndo]) { ! 145: [self undoChange:sender]; ! 146: } else { ! 147: if ([self canRedo]) { ! 148: [self redoChange:sender]; ! 149: } ! 150: } ! 151: return self; ! 152: } ! 153: ! 154: - undoChange:sender ! 155: { ! 156: if ([self canUndo]) { ! 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 objectAt:(_numberOfDoneChanges - 1)]; ! 167: } ! 168: [self changeWasUndone]; ! 169: } ! 170: ! 171: return self; ! 172: } ! 173: ! 174: - redoChange:sender ! 175: { ! 176: if ([self canRedo]) { ! 177: [self disableChanges:self]; ! 178: [_nextChange redoChange]; ! 179: [self enableChanges:self]; ! 180: _lastChange = _nextChange; ! 181: _nextChange = nil; ! 182: _numberOfDoneChanges++; ! 183: _numberOfUndoneChanges--; ! 184: if (_numberOfUndoneChanges > 0) { ! 185: _nextChange = [_changeList objectAt:_numberOfDoneChanges]; ! 186: } ! 187: [self changeWasRedone]; ! 188: } ! 189: ! 190: return self; ! 191: } ! 192: ! 193: - (BOOL)validateCommand:menuCell ! 194: /* ! 195: * See the Draw code for a good example of how validateCommand: ! 196: * can be used to keep the application's menu items up to date. ! 197: */ ! 198: { ! 199: SEL action; ! 200: BOOL canUndo, canRedo, enableMenuItem = YES; ! 201: char menuText[256]; ! 202: ! 203: action = [menuCell action]; ! 204: ! 205: if (action == @selector(undoOrRedoChange:)) { ! 206: enableMenuItem = NO; ! 207: canUndo = [self canUndo]; ! 208: if (canUndo) { ! 209: sprintf(menuText, UNDO_SOMETHING_OPERATION, [_lastChange changeName]); ! 210: enableMenuItem = YES; ! 211: } else { ! 212: canRedo = [self canRedo]; ! 213: if (canRedo) { ! 214: sprintf(menuText, REDO_SOMETHING_OPERATION, [_nextChange changeName]); ! 215: enableMenuItem = YES; ! 216: } else { ! 217: sprintf(menuText, UNDO_OPERATION); ! 218: } ! 219: } ! 220: [self updateMenuCell:menuCell with:menuText]; ! 221: } ! 222: ! 223: if (action == @selector(undoChange:)) { ! 224: canUndo = [self canUndo]; ! 225: if (!canUndo) { ! 226: sprintf(menuText, UNDO_OPERATION); ! 227: } else { ! 228: sprintf(menuText, UNDO_SOMETHING_OPERATION, [_lastChange changeName]); ! 229: } ! 230: [self updateMenuCell:menuCell with:menuText]; ! 231: enableMenuItem = canUndo; ! 232: } ! 233: ! 234: if (action == @selector(redoChange:)) { ! 235: canRedo = [self canRedo]; ! 236: if (!canRedo) { ! 237: sprintf(menuText, REDO_OPERATION); ! 238: } else { ! 239: sprintf(menuText, REDO_SOMETHING_OPERATION, [_nextChange changeName]); ! 240: } ! 241: [self updateMenuCell:menuCell with:menuText]; ! 242: enableMenuItem = canRedo; ! 243: } ! 244: ! 245: return enableMenuItem; ! 246: } ! 247: ! 248: /* Methods called by Change */ ! 249: /* DO NOT call these methods directly */ ! 250: ! 251: - changeInProgress:change ! 252: /* ! 253: * The changeInProgress: and changeComplete: methods are the most ! 254: * complicated part of the undo framework. Their behaviour is documented ! 255: * in the external reference sheets. ! 256: */ ! 257: { ! 258: if (_changesDisabled > 0) { ! 259: [change disable]; ! 260: return nil; ! 261: } ! 262: ! 263: if (_changeInProgress != nil) { ! 264: if ([_changeInProgress incorporateChange:change]) { ! 265: /* ! 266: * The _changeInProgress will keep a pointer to this ! 267: * change and make use of it, but we have no further ! 268: * responsibility for it. ! 269: */ ! 270: [change saveBeforeChange]; ! 271: return self; ! 272: } else { ! 273: /* ! 274: * The _changeInProgress has no more interest in this ! 275: * change than we do, so we'll just disable it. ! 276: */ ! 277: [change disable]; ! 278: return nil; ! 279: } ! 280: } ! 281: ! 282: if (_lastChange != nil) { ! 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 nil; ! 291: } else { ! 292: /* ! 293: * The _lastChange was not able to subsume this change, ! 294: * so we give the _lastChange a chance to finish and then ! 295: * welcome this change as the new _changeInProgress. ! 296: */ ! 297: [_lastChange finishChange]; ! 298: } ! 299: } ! 300: ! 301: /* ! 302: * This will be a new, independent change. ! 303: */ ! 304: [change saveBeforeChange]; ! 305: if (![change disabled]) ! 306: _changeInProgress = change; ! 307: return self; ! 308: } ! 309: ! 310: - changeComplete:change ! 311: /* ! 312: * The changeInProgress: and changeComplete: methods are the most ! 313: * complicated part of the undo framework. Their behaviour is documented ! 314: * in the external reference sheets. ! 315: */ ! 316: { ! 317: int i; ! 318: ! 319: NX_ASSERT(![change changeInProgress], "Fault in Undo system: Code 7"); ! 320: NX_ASSERT(![change disabled], "Fault in Undo system: Code 8"); ! 321: NX_ASSERT([change hasBeenDone], "Fault in Undo system: Code 9"); ! 322: if (change != _changeInProgress) { ! 323: /* ! 324: * "Toto, I don't think we're in Kansas anymore." ! 325: * - Dorthy ! 326: * Actually, we come here whenever a change is ! 327: * incorportated or subsumed by another change ! 328: * and later executes its endChange method. ! 329: */ ! 330: [change saveAfterChange]; ! 331: return nil; ! 332: } ! 333: ! 334: if (_numberOfUndoneChanges > 0) { ! 335: NX_ASSERT(_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 removeObjectAt:(i - 1)] free]; ! 339: } ! 340: _nextChange = nil; ! 341: _numberOfUndoneChanges = 0; ! 342: if (_numberOfDoneChanges < _numberOfDoneChangesAtLastClean) ! 343: _someChangesForgotten = YES; ! 344: } ! 345: if (_numberOfDoneChanges == N_LEVEL_UNDO) { ! 346: NX_ASSERT(_numberOfUndoneChanges == 0, "Fault in Undo system: Code 11"); ! 347: NX_ASSERT(_nextChange == nil, "Fault in Undo system: Code 12"); ! 348: /* ! 349: * The [_changeList removeObjectAt:0] call is order N. ! 350: * This will be slow if N_LEVEL_UNDO is large. ! 351: * Ideally the _changeList should be implemented as ! 352: * a circular queue, or List should do removeObjectAt: ! 353: * in a fixed time. In many applications (including ! 354: * Draw) doing the redisplay associated with the undo ! 355: * will take MUCH longer than even an order N call to ! 356: * removeObjectAt:, so it's not too important that ! 357: * this be changed. ! 358: */ ! 359: [[_changeList removeObjectAt:0] free]; ! 360: _numberOfDoneChanges--; ! 361: _someChangesForgotten = YES; ! 362: } ! 363: [_changeList addObject:change]; ! 364: _numberOfDoneChanges++; ! 365: ! 366: _lastChange = change; ! 367: _changeInProgress = nil; ! 368: ! 369: [change saveAfterChange]; ! 370: [self changeWasDone]; ! 371: ! 372: return self; ! 373: } ! 374: ! 375: /* Methods called by ChangeManager */ ! 376: /* DO NOT call these methods directly */ ! 377: ! 378: - changeWasDone ! 379: /* ! 380: * To be overridden ! 381: */ ! 382: { ! 383: return self; ! 384: } ! 385: ! 386: - changeWasUndone ! 387: /* ! 388: * To be overridden ! 389: */ ! 390: { ! 391: return self; ! 392: } ! 393: ! 394: - changeWasRedone ! 395: /* ! 396: * To be overridden ! 397: */ ! 398: { ! 399: return self; ! 400: } ! 401: ! 402: /* Private Methods */ ! 403: ! 404: - updateMenuCell:menuCell with:(const char *)menuText ! 405: { ! 406: id editMenu, cv; ! 407: ! 408: if (strcmp([menuCell title], menuText)) { ! 409: cv = [menuCell controlView]; ! 410: editMenu = [cv window]; ! 411: #ifdef SIZE_MENU_TO_FIT ! 412: [editMenu disableDisplay]; ! 413: [menuCell setTitle:menuText]; ! 414: [editMenu sizeToFit]; ! 415: [editMenu reenableDisplay]; ! 416: [editMenu display]; ! 417: #else ! 418: if (![editMenu isDisplayEnabled]) [editMenu reenableDisplay]; ! 419: [menuCell setTitle:menuText]; ! 420: #endif ! 421: } ! 422: return self; ! 423: } ! 424: ! 425: @end
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.