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