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 {
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];
42 -(void)_unregisterPerform {
43 if(_performRegistered){
44 _performRegistered=NO;
45 [[NSRunLoop currentRunLoop] cancelPerformSelector:@selector(runLoopUndo:) target:self argument:nil];
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;
65 [self _unregisterPerform];
69 [_currentGroup release];
71 [_actionName release];
76 - (NSArray *)runLoopModes
81 - (NSUInteger)levelsOfUndo
88 return _groupsByEvent;
91 - (void)setRunLoopModes:(NSArray *)modes
94 _modes = [modes retain];
95 [self _unregisterPerform];
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;
112 [self _registerPerform];
114 [self _unregisterPerform];
117 - (BOOL)isUndoRegistrationEnabled
119 return (_disableCount == 0);
122 - (void)disableUndoRegistration
127 - (void)enableUndoRegistration
129 if (_disableCount == 0)
130 [NSException raise:NSInternalInconsistencyException
131 format:@"Attempt to enable registration with no disable message in effect"];
136 - (void)beginUndoGrouping
138 NSUndoGroup *undoGroup = [NSUndoGroup undoGroupWithParentGroup:_currentGroup];
140 if (!([_currentGroup parentGroup] == nil && _state == NSUndoManagerUndoing))
141 [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerCheckpointNotification
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
162 if (parentGroup == nil && [[_currentGroup invocations] count] > 0) {
164 case NSUndoManagerNormal:
165 [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerWillCloseUndoGroupNotification object:self];
167 case NSUndoManagerRedoing:
171 case NSUndoManagerUndoing:
176 [stack addObject:_currentGroup];
177 if (_levelsOfUndo > 0)
178 if ([stack count] > _levelsOfUndo)
179 [stack removeObjectAtIndex:0];
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]];
187 [_currentGroup release];
188 _currentGroup = parentGroup;
191 - (NSInteger)groupingLevel
193 NSUndoGroup *temp = _currentGroup;
194 int level = (_currentGroup != nil);
196 while ((temp = [temp parentGroup])!=nil)
202 - (void)runLoopUndo:(id)dummy
204 if (_groupsByEvent == YES) {
205 if (_currentGroup != nil)
206 [self endUndoGrouping];
207 _performRegistered=NO;
213 if ([_undoStack count] > 0)
216 if ([[_currentGroup invocations] count] > 0)
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
233 [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerWillUndoChangeNotification
236 _state = NSUndoManagerUndoing;
237 undoGroup = [[_undoStack lastObject] retain];
238 [_undoStack removeLastObject];
239 [self beginUndoGrouping];
240 [undoGroup invokeInvocations];
241 [self endUndoGrouping];
243 _state = NSUndoManagerNormal;
245 [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerDidUndoChangeNotification
251 if ([self groupingLevel] == 1)
252 [self endUndoGrouping];
254 [self undoNestedGroup];
259 return (_state == NSUndoManagerUndoing);
265 [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerCheckpointNotification
267 return ([_redoStack count] > 0);
272 NSUndoGroup *undoGroup;
274 if (_state == NSUndoManagerUndoing)
275 [NSException raise:NSInternalInconsistencyException
276 format:@"redo called while undoing"];
278 [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerCheckpointNotification
281 [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerWillRedoChangeNotification
284 _state = NSUndoManagerRedoing;
285 undoGroup = [[_redoStack lastObject] retain];
286 [_redoStack removeLastObject];
287 [self beginUndoGrouping];
288 [undoGroup invokeInvocations];
289 [self endUndoGrouping];
291 _state = NSUndoManagerNormal;
293 [[NSNotificationCenter defaultCenter] postNotificationName:NSUndoManagerDidRedoChangeNotification
300 return (_state == NSUndoManagerRedoing);
303 - (void)registerUndoWithTarget:(id)target selector:(SEL)selector object:(id)object
305 NSInvocation *invocation;
306 NSMethodSignature *signature;
308 if (_disableCount > 0)
311 if (_groupsByEvent && _currentGroup == nil) {
312 [self _registerPerform];
313 [self beginUndoGrouping];
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];
341 - (void)removeAllActionsWithTarget:(id)target
343 NSUndoGroup *undoGroup;
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];
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];
364 - (id)prepareWithInvocationTarget:(id)target
366 _preparedTarget = [target retain];
371 -(NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
372 return [_preparedTarget methodSignatureForSelector:selector];
375 - (void)forwardInvocation:(NSInvocation *)invocation
377 if (_disableCount > 0)
380 if (_preparedTarget == nil)
381 [NSException raise:NSInternalInconsistencyException
382 format:@"forwardInvocation called without first preparing a target"];
384 if (_groupsByEvent && _currentGroup == nil) {
385 [self _registerPerform];
386 [self beginUndoGrouping];
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];
400 [_preparedTarget release];
401 _preparedTarget = nil;
404 - (void)setActionName:(NSString *)name
406 [_actionName release];
407 _actionName = [name retain];
410 - (NSString *)undoActionName
418 - (NSString *)undoMenuItemTitle
420 return [self undoMenuTitleForUndoActionName:[self undoActionName]];
423 // needs localization
424 - (NSString *)undoMenuTitleForUndoActionName:(NSString *)name
427 if ([name length] > 0)
428 return [NSString stringWithFormat:@"Undo %@", name];
436 - (NSString *)redoActionName
444 - (NSString *)redoMenuItemTitle
446 return [self redoMenuTitleForUndoActionName:[self redoActionName]];
449 - (NSString *)redoMenuTitleForUndoActionName:(NSString *)name
452 if ([name length] > 0)
453 return [NSString stringWithFormat:@"Redo %@", name];
461 - (void)clearRedoStackIfStateIsNormal
463 if (_state == NSUndoManagerNormal)
464 [_redoStack removeAllObjects];