Merge pull request #9 from gunyarakun/fix-typo
[cocotron.git] / Foundation / NSUndoManager / NSUndoManager.m
blob259eeaacd700a6345945b0185b9df31ead91b7b7
1 /* Copyright (c) 2006-2007 Christopher J. W. Lloyd
3 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
8 #import <Foundation/NSUndoManager.h>
9 #import <Foundation/NSUndoGroup.h>
10 #import <Foundation/NSString.h>
11 #import <Foundation/NSArray.h>
12 #import <Foundation/NSRunLoop.h>
13 #import <Foundation/NSNotificationCenter.h>
14 #import <Foundation/NSException.h>
15 #import <Foundation/NSRaise.h>
16 #import <Foundation/NSInvocation.h>
18 enum _NSUndoManagerState {
19     NSUndoManagerNormal,
20     NSUndoManagerUndoing,
21     NSUndoManagerRedoing
24 NSString * const NSUndoManagerCheckpointNotification=@"NSUndoManagerCheckpointNotification";
25 NSString * const NSUndoManagerDidOpenUndoGroupNotification=@"NSUndoManagerDidOpenUndoGroupNotification";
26 NSString * const NSUndoManagerWillCloseUndoGroupNotification=@"NSUndoManagerWillCloseUndoGroupNotification";
27 NSString * const NSUndoManagerWillUndoChangeNotification=@"NSUndoManagerWillUndoChangeNotification";
28 NSString * const NSUndoManagerDidUndoChangeNotification=@"NSUndoManagerDidUndoChangeNotification";
29 NSString * const NSUndoManagerWillRedoChangeNotification=@"NSUndoManagerWillRedoChangeNotification";
30 NSString * const NSUndoManagerDidRedoChangeNotification=@"NSUndoManagerDidRedoChangeNotification";
32 @implementation NSUndoManager
34 -(void)_registerPerform {
35    if(!_performRegistered){
36     _performRegistered=YES;
37     [[NSRunLoop currentRunLoop] performSelector:@selector(runLoopUndo:) target:self argument:nil order:NSUndoCloseGroupingRunLoopOrdering modes:_modes];
39    }
42 -(void)_unregisterPerform {
43    if(_performRegistered){
44     _performRegistered=NO;
45     [[NSRunLoop currentRunLoop] cancelPerformSelector:@selector(runLoopUndo:) target:self argument:nil];    
46    }
50 - (id)init
52     _undoStack = [[NSMutableArray alloc] init];
53     _redoStack = [[NSMutableArray alloc] init];
54     _state = NSUndoManagerNormal;
56     [self setRunLoopModes:[NSArray arrayWithObject:NSDefaultRunLoopMode]];
57     [self setGroupsByEvent:YES];
58     _performRegistered=NO;
60     return self;
63 - (void)dealloc
65    [self _unregisterPerform];
67     [_undoStack release];
68     [_redoStack release];
69     [_currentGroup release];
70     [_modes release];
71     [_actionName release];
72         
73     [super dealloc];
76 - (NSArray *)runLoopModes
78     return _modes;
81 - (NSUInteger)levelsOfUndo
83     return _levelsOfUndo;
86 - (BOOL)groupsByEvent
88     return _groupsByEvent;
91 - (void)setRunLoopModes:(NSArray *)modes
93     [_modes release];
94     _modes = [modes retain];
95    [self _unregisterPerform];
96    if (_groupsByEvent)
97     [self _registerPerform];
100 - (void)setLevelsOfUndo:(NSUInteger)levels
102     _levelsOfUndo = levels;
103     while ([_undoStack count] > _levelsOfUndo)
104         [_undoStack removeObjectAtIndex:0];
105     while ([_redoStack count] > _levelsOfUndo)
106         [_redoStack removeObjectAtIndex:0];
109 - (void)setGroupsByEvent:(BOOL)flag {
110     _groupsByEvent = flag;
111    if (_groupsByEvent)
112     [self _registerPerform];
113    else
114     [self _unregisterPerform];
117 - (BOOL)isUndoRegistrationEnabled
119     return (_disableCount == 0);
122 - (void)disableUndoRegistration
124     _disableCount++;
127 - (void)enableUndoRegistration
129     if (_disableCount == 0)
130         [NSException raise:NSInternalInconsistencyException
131                     format:@"Attempt to enable registration with no disable message in effect"];
132     
133     _disableCount--;
136 - (void)beginUndoGrouping
138     NSUndoGroup *undoGroup = [NSUndoGroup undoGroupWithParentGroup:_currentGroup];
140     if (!([_currentGroup parentGroup] == nil && _state == NSUndoManagerUndoing))
141         [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerCheckpointNotification
142                                                             object:self];
144     [_currentGroup release];
145     _currentGroup = [undoGroup retain];
147     [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerDidOpenUndoGroupNotification object:self];
150 - (void)endUndoGrouping
152     NSMutableArray *stack = nil;
153     NSUndoGroup *parentGroup = [[_currentGroup parentGroup] retain];
155     if (_currentGroup == nil)
156         [NSException raise:NSInternalInconsistencyException
157                     format:@"endUndoGrouping called without first calling beginUndoGrouping"];
159     [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerCheckpointNotification
160                                                         object:self];
162     if (parentGroup == nil && [[_currentGroup invocations] count] > 0) {
163         switch (_state) {
164             case NSUndoManagerNormal:
165                 [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerWillCloseUndoGroupNotification object:self];
166                 
167             case NSUndoManagerRedoing:
168                 stack = _undoStack;
169                 break;
171             case NSUndoManagerUndoing:
172                 stack = _redoStack;
173                 break;
174         }
176         [stack addObject:_currentGroup];
177         if (_levelsOfUndo > 0)
178             if ([stack count] > _levelsOfUndo)
179                 [stack removeObjectAtIndex:0];
180     }
181     else {
182         // a nested group was closed. fold its invocations into its parent, preserving the
183         // order for future changes on the parent.
184         [parentGroup addInvocationsFromArray:[_currentGroup invocations]];
185     }
187     [_currentGroup release];
188     _currentGroup = parentGroup;
191 - (NSInteger)groupingLevel
193     NSUndoGroup *temp = _currentGroup;
194     int level = (_currentGroup != nil);
196     while ((temp = [temp parentGroup])!=nil)
197         level++;
199     return level;
202 - (void)runLoopUndo:(id)dummy
204     if (_groupsByEvent == YES) {
205         if (_currentGroup != nil)
206             [self endUndoGrouping];
207                 _performRegistered=NO;
208     }
211 - (BOOL)canUndo
213     if ([_undoStack count] > 0)
214         return YES;
216     if ([[_currentGroup invocations] count] > 0)
217         return YES;
218     
219     return NO;
222 - (void)undoNestedGroup
224     NSUndoGroup *undoGroup;
226     if (_currentGroup != nil)
227         [NSException raise:NSInternalInconsistencyException
228                     format:@"undo called with open nested group"];
230     [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerCheckpointNotification
231                                                         object:self];
233     [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerWillUndoChangeNotification
234                                                         object:self];
236     _state = NSUndoManagerUndoing;
237     undoGroup = [[_undoStack lastObject] retain];
238     [_undoStack removeLastObject];
239     [self beginUndoGrouping];
240     [undoGroup invokeInvocations];
241     [self endUndoGrouping];
242     [undoGroup release];
243     _state = NSUndoManagerNormal;
245     [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerDidUndoChangeNotification
246                                                         object:self];
249 - (void)undo
251     if ([self groupingLevel] == 1)
252         [self endUndoGrouping];
253     
254     [self undoNestedGroup];
257 - (BOOL)isUndoing
259     return (_state == NSUndoManagerUndoing);
263 - (BOOL)canRedo
265     [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerCheckpointNotification
266                                                         object:self];
267     return ([_redoStack count] > 0);
270 - (void)redo
272     NSUndoGroup *undoGroup;
274     if (_state == NSUndoManagerUndoing)
275         [NSException raise:NSInternalInconsistencyException
276                     format:@"redo called while undoing"];
278     [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerCheckpointNotification
279                                                         object:self];
281     [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerWillRedoChangeNotification
282                                                         object:self];
284     _state = NSUndoManagerRedoing;
285     undoGroup = [[_redoStack lastObject] retain];
286     [_redoStack removeLastObject];
287     [self beginUndoGrouping];
288     [undoGroup invokeInvocations];
289     [self endUndoGrouping];
290     [undoGroup release];
291     _state = NSUndoManagerNormal;
293     [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerDidRedoChangeNotification
294                                                         object:self];
298 - (BOOL)isRedoing
300     return (_state == NSUndoManagerRedoing);
303 - (void)registerUndoWithTarget:(id)target selector:(SEL)selector object:(id)object
305     NSInvocation *invocation;
306     NSMethodSignature *signature;
307     
308     if (_disableCount > 0)
309         return;
311         if (_groupsByEvent && _currentGroup == nil) {
312                 [self _registerPerform];
313         [self beginUndoGrouping];
314         }               
315         
316     if (_currentGroup == nil)
317         [NSException raise:NSInternalInconsistencyException
318                     format:@"forwardInvocation called without first opening an undo group"];
320     signature = [target methodSignatureForSelector:selector];
321     invocation = [NSInvocation invocationWithMethodSignature:signature];
323     [invocation setTarget:target];
324     [invocation setSelector:selector];
325     [invocation setArgument:&object atIndex:2];
326     [invocation retainArguments];
328     [_currentGroup addInvocation:invocation];
330     if (_state == NSUndoManagerNormal)
331         [_redoStack removeAllObjects];
334 - (void)removeAllActions
336     [_undoStack removeAllObjects];
337     [_redoStack removeAllObjects];
338     _disableCount = 0;
341 - (void)removeAllActionsWithTarget:(id)target
343     NSUndoGroup *undoGroup;
344     int i;
346     [_currentGroup removeInvocationsWithTarget:target];
348     for (i = 0; i < [_undoStack count]; ++i) {
349         undoGroup = [_undoStack objectAtIndex:i];
351         [undoGroup removeInvocationsWithTarget:target];
352         if ([[undoGroup invocations] count] == 0)
353             [_undoStack removeObject:undoGroup];
354     }
355     for (i = 0; i < [_redoStack count]; ++i) {
356         undoGroup = [_redoStack objectAtIndex:i];
358         [undoGroup removeInvocationsWithTarget:target];
359         if ([[undoGroup invocations] count] == 0)
360             [_redoStack removeObject:undoGroup];
361     }
364 - (id)prepareWithInvocationTarget:(id)target
366     _preparedTarget = [target retain];
367     
368     return self;
371 -(NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
372     return [_preparedTarget methodSignatureForSelector:selector];
375 - (void)forwardInvocation:(NSInvocation *)invocation
377     if (_disableCount > 0)
378         return;
379     
380     if (_preparedTarget == nil)
381         [NSException raise:NSInternalInconsistencyException
382                     format:@"forwardInvocation called without first preparing a target"];
383         
384         if (_groupsByEvent && _currentGroup == nil) {
385                 [self _registerPerform];
386                 [self beginUndoGrouping];
387         }               
388         
389     if (_currentGroup == nil)
390         [NSException raise:NSInternalInconsistencyException
391                     format:@"forwardInvocation called without first opening an undo group"];
393     [invocation setTarget:_preparedTarget];
394     [_currentGroup addInvocation:invocation];
395     [invocation retainArguments];
397     if (_state == NSUndoManagerNormal)
398         [_redoStack removeAllObjects];
399     
400     [_preparedTarget release];
401     _preparedTarget = nil;
404 - (void)setActionName:(NSString *)name
406     [_actionName release];
407     _actionName = [name retain];
410 - (NSString *)undoActionName
412     if ([self canUndo])
413         return _actionName;
414     
415     return nil;
418 - (NSString *)undoMenuItemTitle
420     return [self undoMenuTitleForUndoActionName:[self undoActionName]];
423 // needs localization
424 - (NSString *)undoMenuTitleForUndoActionName:(NSString *)name
426     if (name != nil) {
427         if ([name length] > 0)
428             return [NSString stringWithFormat:@"Undo %@", name];
430         return @"Undo";
431     }
433     return name;
436 - (NSString *)redoActionName
438     if ([self canRedo])
439         return _actionName;
441     return nil;
444 - (NSString *)redoMenuItemTitle
446     return [self redoMenuTitleForUndoActionName:[self redoActionName]];
449 - (NSString *)redoMenuTitleForUndoActionName:(NSString *)name
451     if (name != nil) {
452         if ([name length] > 0)
453             return [NSString stringWithFormat:@"Redo %@", name];
455         return @"Redo";
456     }
458     return name;
461 - (void)clearRedoStackIfStateIsNormal
463   if (_state == NSUndoManagerNormal)
464       [_redoStack removeAllObjects];
467 @end