API  0.9.7
 All Classes Files Functions Variables Macros Groups Pages
CPUndoManager.j
Go to the documentation of this file.
1 /*
2  * CPUndoManager.j
3  * Foundation
4  *
5  * Created by Francisco Tolmasky.
6  * Copyright 2008, 280 North, Inc.
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Lesser General Public
10  * License as published by the Free Software Foundation; either
11  * version 2.1 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16  * Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public
19  * License along with this library; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21  */
22 
23 
27 
28 CPUndoManagerCheckpointNotification = @"CPUndoManagerCheckpointNotification";
29 CPUndoManagerDidOpenUndoGroupNotification = @"CPUndoManagerDidOpenUndoGroupNotification";
30 CPUndoManagerDidCloseUndoGroupNotification = @"CPUndoManagerDidCloseUndoGroupNotification";
31 CPUndoManagerDidRedoChangeNotification = @"CPUndoManagerDidRedoChangeNotification";
32 CPUndoManagerDidUndoChangeNotification = @"CPUndoManagerDidUndoChangeNotification";
33 CPUndoManagerWillCloseUndoGroupNotification = @"CPUndoManagerWillCloseUndoGroupNotification";
34 CPUndoManagerWillRedoChangeNotification = @"CPUndoManagerWillRedoChangeNotification";
35 CPUndoManagerWillUndoChangeNotification = @"CPUndoManagerWillUndoChangeNotification";
36 
38 
39 var _CPUndoGroupingPool = [],
40  _CPUndoGroupingPoolCapacity = 5;
41 
42 /* @ignore */
43 @implementation _CPUndoGrouping : CPObject
44 {
45  _CPUndoGrouping _parent;
46  CPMutableArray _invocations;
47  CPString _actionName;
48 }
49 
50 + (void)_poolUndoGrouping:(_CPUndoGrouping)anUndoGrouping
51 {
52  if (!anUndoGrouping || _CPUndoGroupingPool.length >= _CPUndoGroupingPoolCapacity)
53  return;
54 
55  _CPUndoGroupingPool.push(anUndoGrouping);
56 }
57 
58 + (id)undoGroupingWithParent:(_CPUndoGrouping)anUndoGrouping
59 {
60  if (_CPUndoGroupingPool.length)
61  {
62  var grouping = _CPUndoGroupingPool.pop();
63 
64  grouping._parent = anUndoGrouping;
65 
66  if (grouping._invocations.length)
67  grouping._invocations = [];
68 
69  return grouping;
70  }
71 
72  return [[self alloc] initWithParent:anUndoGrouping];
73 }
74 
75 - (id)initWithParent:(_CPUndoGrouping)anUndoGrouping
76 {
77  self = [super init];
78 
79  if (self)
80  {
81  _parent = anUndoGrouping;
82  _invocations = [];
83  _actionName = @"";
84  }
85 
86  return self;
87 }
88 
89 - (_CPUndoGrouping)parent
90 {
91  return _parent;
92 }
93 
94 - (void)addInvocation:(CPInvocation)anInvocation
95 {
96  _invocations.push(anInvocation);
97 }
98 
99 - (void)addInvocationsFromArray:(CPArray)invocations
100 {
101  [_invocations addObjectsFromArray:invocations];
102 }
103 
104 - (BOOL)removeInvocationsWithTarget:(id)aTarget
105 {
106  var index = _invocations.length;
107 
108  while (index--)
109  if ([_invocations[index] target] == aTarget)
110  _invocations.splice(index, 1);
111 }
112 
113 - (CPArray)invocations
114 {
115  return _invocations;
116 }
117 
118 - (void)invoke
119 {
120  var index = _invocations.length;
121 
122  while (index--)
123  [_invocations[index] invoke];
124 }
125 
126 - (void)setActionName:(CPString)aName
127 {
128  _actionName = aName;
129 }
130 
131 - (CPString)actionName
132 {
133  return _actionName;
134 }
135 
136 @end
137 
138 var _CPUndoGroupingParentKey = @"_CPUndoGroupingParentKey",
139  _CPUndoGroupingInvocationsKey = @"_CPUndoGroupingInvocationsKey",
140  _CPUndoGroupingActionNameKey = @"_CPUndoGroupingActionNameKey";
141 
142 @implementation _CPUndoGrouping (CPCoder)
143 
144 - (id)initWithCoder:(CPCoder)aCoder
145 {
146  self = [super init];
147 
148  if (self)
149  {
150  _parent = [aCoder decodeObjectForKey:_CPUndoGroupingParentKey];
151  _invocations = [aCoder decodeObjectForKey:_CPUndoGroupingInvocationsKey];
152  _actionName = [aCoder decodeObjectForKey:_CPUndoGroupingActionNameKey];
153  }
154 
155  return self;
156 }
157 
158 - (void)encodeWithCoder:(CPCoder)aCoder
159 {
160  [aCoder encodeObject:_parent forKey:_CPUndoGroupingParentKey];
161  [aCoder encodeObject:_invocations forKey:_CPUndoGroupingInvocationsKey];
162  [aCoder encodeObject:_actionName forKey:_CPUndoGroupingActionNameKey];
163 }
164 
165 @end
166 
182 @implementation CPUndoManager : CPObject
183 {
184  CPMutableArray _redoStack;
185  CPMutableArray _undoStack;
186 
187  BOOL _groupsByEvent;
188  int _disableCount;
189  int _levelsOfUndo;
190  id _currentGrouping;
191  int _state;
192 
193  id _preparedTarget;
194  id _undoManagerProxy;
195 
196  CPArray _runLoopModes;
197  BOOL _registeredWithRunLoop;
198 }
199 
204 - (id)init
205 {
206  self = [super init];
207 
208  if (self)
209  {
210  _redoStack = [];
211  _undoStack = [];
212 
213  _disableCount = 0;
214  _state = CPUndoManagerNormal;
215 
216  [self setRunLoopModes:[CPDefaultRunLoopMode]];
217  [self setGroupsByEvent:YES];
218 
219  _undoManagerProxy = [_CPUndoManagerProxy alloc];
220  _undoManagerProxy._undoManager = self;
221  }
222 
223  return self;
224 }
225 
226 - (void)_addUndoInvocation:(CPInvocation)anInvocation
227 {
228  if (!_currentGrouping)
229  // Remember that we create these lazily...
230  if ([self groupsByEvent])
231  [self _beginUndoGroupingForEvent];
232  else
233  [CPException raise:CPInternalInconsistencyException reason:"No undo group is currently open"];
234 
235  [_currentGrouping addInvocation:anInvocation];
236 
237  if (_state === CPUndoManagerNormal)
238  [_redoStack removeAllObjects];
239 }
240 
241 // Registering Undo Operations
249 - (void)registerUndoWithTarget:(id)aTarget selector:(SEL)aSelector object:(id)anObject
250 {
251  // Don't do anything if we're disabled.
252  if (_disableCount > 0)
253  return;
254 
255  //signature = [target methodSignatureForSelector:selector];
256  // FIXME: we need method signatures.
257  var invocation = [CPInvocation invocationWithMethodSignature:nil];
258 
259  [invocation setTarget:aTarget];
260  [invocation setSelector:aSelector];
261  [invocation setArgument:anObject atIndex:2];
262 
263  [self _addUndoInvocation:invocation];
264 }
270 - (id)prepareWithInvocationTarget:(id)aTarget
271 {
272  _preparedTarget = aTarget;
273 
274  return _undoManagerProxy;
275 }
276 
277 /*
278  FIXME This method doesn't seem to do anything right
279  @ignore
280 */
281 - (CPMethodSignature)_methodSignatureOfPreparedTargetForSelector:(SEL)aSelector
282 {
283  if ([_preparedTarget respondsToSelector:aSelector])
284  return 1;
285 
286  return nil;//[_preparedTarget methodSignatureForSelector:selector];
287 }
288 
294 - (void)_forwardInvocationToPreparedTarget:(CPInvocation)anInvocation
295 {
296  // Don't do anything if we're disabled.
297  if (_disableCount > 0)
298  return;
299 
300 /*
301 if (_currentGroup == nil)
302  [NSException raise:NSInternalInconsistencyException
303  format:@"forwardInvocation called without first opening an undo group"];
304 */
305  [anInvocation setTarget:_preparedTarget];
306 
307  [self _addUndoInvocation:anInvocation];
308 
309  _preparedTarget = nil;
310 }
311 
312 // Checking Undo Ability
316 - (BOOL)canRedo
317 {
319  postNotificationName:CPUndoManagerCheckpointNotification
320  object:self];
321 
322  return [_redoStack count] > 0;
323 }
324 
328 - (BOOL)canUndo
329 {
330  if (_undoStack.length > 0)
331  return YES;
332 
333  return [[_currentGrouping invocations] count] > 0;
334 }
335 
336 // Preform Undo and Redo
340 - (void)undo
341 {
342  if ([self groupingLevel] === 1)
343  [self endUndoGrouping];
344 
345  [self undoNestedGroup];
346 }
347 
351 - (void)undoNestedGroup
352 {
353  if ([_undoStack count] <= 0)
354  return;
355 
356  var defaultCenter = [CPNotificationCenter defaultCenter];
357 
358  [defaultCenter postNotificationName:CPUndoManagerCheckpointNotification
359  object:self];
360 
361  [defaultCenter postNotificationName:CPUndoManagerWillUndoChangeNotification
362  object:self];
363 
364  var undoGrouping = _undoStack.pop(),
365  actionName = [undoGrouping actionName];
366 
367  _state = CPUndoManagerUndoing;
368 
369  [self _beginUndoGrouping];
370  [undoGrouping invoke];
371  [self endUndoGrouping];
372 
373  [_CPUndoGrouping _poolUndoGrouping:undoGrouping];
374 
375  _state = CPUndoManagerNormal;
376 
377  [[_redoStack lastObject] setActionName:actionName];
378 
379  [defaultCenter postNotificationName:CPUndoManagerDidUndoChangeNotification
380  object:self];
381 }
382 
386 - (void)redo
387 {
388  // Don't do anything if we have no redos.
389  if ([_redoStack count] <= 0)
390  return;
391 
392 /* if (_state == NSUndoManagerUndoing)
393  [NSException raise:NSInternalInconsistencyException
394  format:@"redo called while undoing"];
395 */
396 
397  var defaultCenter = [CPNotificationCenter defaultCenter];
398 
399  [defaultCenter postNotificationName:CPUndoManagerCheckpointNotification
400  object:self];
401 
402  [defaultCenter postNotificationName:CPUndoManagerWillRedoChangeNotification
403  object:self];
404 
405  var oldUndoGrouping = _currentGrouping,
406  undoGrouping = _redoStack.pop(),
407  actionName = [undoGrouping actionName];
408 
409  _currentGrouping = nil;
410  _state = CPUndoManagerRedoing;
411 
412  [self _beginUndoGrouping];
413  [undoGrouping invoke];
414  [self endUndoGrouping];
415 
416  [_CPUndoGrouping _poolUndoGrouping:undoGrouping];
417 
418  _currentGrouping = oldUndoGrouping;
419  _state = CPUndoManagerNormal;
420 
421  [[_undoStack lastObject] setActionName:actionName];
422  [defaultCenter postNotificationName:CPUndoManagerDidRedoChangeNotification object:self];
423 }
424 
425 // Creating Undo Groups
429 - (void)beginUndoGrouping
430 {
431  // It doesn't matter that the user is creating a group themselves, we are
432  // pretending to have opened the group at the beginning of the run loop,
433  // so create an implicit one here.
434  if (!_currentGrouping && [self groupsByEvent])
435  [self _beginUndoGroupingForEvent];
436 
438  postNotificationName:CPUndoManagerCheckpointNotification
439  object:self];
440 
441  [self _beginUndoGrouping];
442 }
443 
444 /* @ignore */
445 - (void)_beginUndoGroupingForEvent
446 {
447  [self _beginUndoGrouping];
448  [self _registerWithRunLoop];
449 }
450 
451 /* @ignore */
452 - (void)_beginUndoGrouping
453 {
454  _currentGrouping = [_CPUndoGrouping undoGroupingWithParent:_currentGrouping];
455 }
456 
461 - (void)endUndoGrouping
462 {
463  if (!_currentGrouping)
464  [CPException raise:CPInternalInconsistencyException reason:"endUndoGrouping. No undo group is currently open."];
465 
466  var defaultCenter = [CPNotificationCenter defaultCenter];
467 
468  [defaultCenter postNotificationName:CPUndoManagerCheckpointNotification
469  object:self];
470 
471  var parent = [_currentGrouping parent];
472 
473  if (!parent && [_currentGrouping invocations].length > 0)
474  {
475  [defaultCenter
476  postNotificationName:CPUndoManagerWillCloseUndoGroupNotification
477  object:self];
478 
479  // Put this group on the redo stack if we are currently undoing, otherwise
480  // put it on the undo stack. That way, "undos" become "redos".
481  var stack = _state === CPUndoManagerUndoing ? _redoStack : _undoStack;
482 
483  stack.push(_currentGrouping);
484 
485  if (_levelsOfUndo > 0 && stack.length > _levelsOfUndo)
486  stack.splice(0, 1);
487 
488  [defaultCenter
489  postNotificationName:CPUndoManagerDidCloseUndoGroupNotification
490  object:self];
491  }
492 
493  // Nested Undo Grouping
494  else
495  {
496  [parent addInvocationsFromArray:[_currentGrouping invocations]];
497 
498  [_CPUndoGrouping _poolUndoGrouping:_currentGrouping];
499  }
500 
501  _currentGrouping = parent;
502 }
503 
510 - (void)enableUndoRegistration
511 {
512  if (_disableCount <= 0)
513  [CPException raise:CPInternalInconsistencyException
514  reason:"enableUndoRegistration. There are no disable messages in effect right now."];
515 
516  _disableCount--;
517 }
518 
522 - (BOOL)groupsByEvent
523 {
524  return _groupsByEvent;
525 }
526 
531 - (void)setGroupsByEvent:(BOOL)aFlag
532 {
533  aFlag = !!aFlag;
534 
535  if (_groupsByEvent === aFlag)
536  return;
537 
538  _groupsByEvent = aFlag;
539 
540  if (![self groupsByEvent])
541  [self _unregisterWithRunLoop];
542 }
543 
547 - (unsigned)groupingLevel
548 {
549  var grouping = _currentGrouping,
550  level = _currentGrouping ? 1 : 0;
551 
552  while (grouping = [grouping parent])
553  ++level;
554 
555  return level;
556 }
557 
558 // Disabling Undo
562 - (void)disableUndoRegistration
563 {
564  ++_disableCount;
565 }
566 
570 - (BOOL)isUndoRegistrationEnabled
571 {
572  return _disableCount == 0;
573 }
574 
575 // Checking Whether Undo or Redo Is Being Performed
579 - (BOOL)isUndoing
580 {
581  return _state === CPUndoManagerUndoing;
582 }
583 
587 - (BOOL)isRedoing
588 {
589  return _state === CPUndoManagerRedoing;
590 }
591 
592 // Clearing Undo Operations
596 - (void)removeAllActions
597 {
598  // Close off any groupings.
599  while (_currentGrouping)
600  [self endUndoGrouping];
601 
602  // Won't need this anymore
603  [self _unregisterWithRunLoop];
604 
605  _state = CPUndoManagerNormal;
606  _redoStack = [];
607  _undoStack = [];
608  _disableCount = 0;
609 }
610 
615 - (void)removeAllActionsWithTarget:(id)aTarget
616 {
617  [_currentGrouping removeInvocationsWithTarget:aTarget];
618 
619  var index = _redoStack.length;
620 
621  while (index--)
622  {
623  var grouping = _redoStack[index];
624 
625  [grouping removeInvocationsWithTarget:aTarget];
626 
627  if (![grouping invocations].length)
628  _redoStack.splice(index, 1);
629  }
630 
631  index = _undoStack.length;
632 
633  while (index--)
634  {
635  var grouping = _undoStack[index];
636 
637  [grouping removeInvocationsWithTarget:aTarget];
638 
639  if (![grouping invocations].length)
640  _undoStack.splice(index, 1);
641  }
642 }
643 
644 // Managing the Action Name
650 - (void)setActionName:(CPString)anActionName
651 {
652  if (anActionName !== nil && _currentGrouping)
653  [_currentGrouping setActionName:anActionName];
654 }
655 
662 - (CPString)redoActionName
663 {
664  if (![self canRedo])
665  return nil;
666 
667  return [[_redoStack lastObject] actionName];
668 }
669 
675 - (CPString)redoMenuItemTitle
676 {
677  return [self redoMenuTitleForUndoActionName:[self redoActionName]];
678 }
679 
685 - (CPString)redoMenuTitleForUndoActionName:(CPString)anActionName
686 {
687  // This handles the empty string ("") case as well.
688  if (anActionName || anActionName === 0)
689 
690  // FIXME: The terms @"Redo" and @"Redo %@" should be localized.
691  // KEYWORDS: Localization
692  return @"Redo " + anActionName;
693 
694  return @"Redo";
695 }
696 
703 - (CPString)undoActionName
704 {
705  if (![self canUndo])
706  return nil;
707 
708  return [[_undoStack lastObject] actionName];
709 }
710 
716 - (CPString)undoMenuItemTitle
717 {
718  return [self undoMenuTitleForUndoActionName:[self undoActionName]];
719 }
720 
726 - (CPString)undoMenuTitleForUndoActionName:(CPString)anActionName
727 {
728  // This handles the empty string ("") case as well.
729  if (anActionName || anActionName === 0)
730 
731  // FIXME: The terms @"Undo" and @"Undo %@" should be localized.
732  // KEYWORDS: Localization
733  return @"Undo " + anActionName;
734 
735  return @"Undo";
736 }
737 
738 // Working With Run Loops
743 - (CPArray)runLoopModes
744 {
745  return _runLoopModes;
746 }
747 
756 - (void)setRunLoopModes:(CPArray)modes
757 {
758  _runLoopModes = [modes copy];
759 
760  if (_registeredWithRunLoop)
761  {
762  [self _unregisterWithRunLoop];
763  [self _registerWithRunLoop];
764  }
765 }
766 
767 - (void)_runLoopEndUndoGrouping
768 {
769  [self endUndoGrouping];
770  _registeredWithRunLoop = NO;
771 }
772 
773 /* @ignore */
774 - (void)_registerWithRunLoop
775 {
776  if (_registeredWithRunLoop)
777  return;
778 
779  _registeredWithRunLoop = YES;
781  performSelector:@selector(_runLoopEndUndoGrouping)
782  target:self
783  argument:nil
784  order:CPUndoCloseGroupingRunLoopOrdering
785  modes:_runLoopModes];
786 }
787 
788 /* @ignore */
789 - (void)_unregisterWithRunLoop
790 {
791  if (!_registeredWithRunLoop)
792  return;
793 
794  _registeredWithRunLoop = NO;
796  cancelPerformSelector:@selector(_runLoopEndUndoGrouping)
797  target:self
798  argument:nil];
799 }
800 
801 - (void)observeChangesForKeyPath:(CPString)aKeyPath ofObject:(id)anObject
802 {
803  [anObject addObserver:self
804  forKeyPath:aKeyPath
805  options:CPKeyValueObservingOptionOld | CPKeyValueObservingOptionNew
806  context:NULL];
807 }
808 
809 - (void)stopObservingChangesForKeyPath:(CPString)aKeyPath ofObject:(id)anObject
810 {
811  [anObject removeObserver:self forKeyPath:aKeyPath];
812 }
813 
814 - (void)observeValueForKeyPath:(CPString)aKeyPath
815  ofObject:(id)anObject
816  change:(CPDictionary)aChange
817  context:(id)aContext
818 {
819  // Don't add no-ops to the undo stack.
820  var before = [aChange valueForKey:CPKeyValueChangeOldKey],
821  after = [aChange valueForKey:CPKeyValueChangeNewKey];
822  if (before === after || (before !== nil && before.isa && (after === nil || after.isa) && [before isEqual:after]))
823  return;
824 
825  [[self prepareWithInvocationTarget:anObject]
827  toKeyPath:aKeyPath];
828 }
829 
830 @end
831 
832 var CPUndoManagerRedoStackKey = @"CPUndoManagerRedoStackKey",
833  CPUndoManagerUndoStackKey = @"CPUndoManagerUndoStackKey",
834 
835  CPUndoManagerLevelsOfUndoKey = @"CPUndoManagerLevelsOfUndoKey",
836  CPUndoManagerActionNameKey = @"CPUndoManagerActionNameKey",
837  CPUndoManagerCurrentGroupingKey = @"CPUndoManagerCurrentGroupingKey",
838 
839  CPUndoManagerRunLoopModesKey = @"CPUndoManagerRunLoopModesKey",
840  CPUndoManagerGroupsByEventKey = @"CPUndoManagerGroupsByEventKey";
841 
843 
844 - (id)initWithCoder:(CPCoder)aCoder
845 {
846  self = [super init];
847 
848  if (self)
849  {
850  _redoStack = [aCoder decodeObjectForKey:CPUndoManagerRedoStackKey];
851  _undoStack = [aCoder decodeObjectForKey:CPUndoManagerUndoStackKey];
852 
853  _levelsOfUndo = [aCoder decodeObjectForKey:CPUndoManagerLevelsOfUndoKey];
854 // _actionName = [aCoder decodeObjectForKey:CPUndoManagerActionNameKey];
855  _currentGrouping = [aCoder decodeObjectForKey:CPUndoManagerCurrentGroupingKey];
856 
857  _state = CPUndoManagerNormal;
858 
859  [self setRunLoopModes:[aCoder decodeObjectForKey:CPUndoManagerRunLoopModesKey]];
860  [self setGroupsByEvent:[aCoder decodeBoolForKey:CPUndoManagerGroupsByEventKey]];
861  }
862 
863  return self;
864 }
865 
866 - (void)encodeWithCoder:(CPCoder)aCoder
867 {
868  [aCoder encodeObject:_redoStack forKey:CPUndoManagerRedoStackKey];
869  [aCoder encodeObject:_undoStack forKey:CPUndoManagerUndoStackKey];
870 
871  [aCoder encodeInt:_levelsOfUndo forKey:CPUndoManagerLevelsOfUndoKey];
872 // [aCoder encodeObject:_actionName forKey:CPUndoManagerActionNameKey];
873 
874  [aCoder encodeObject:_currentGrouping forKey:CPUndoManagerCurrentGroupingKey];
875 
876  [aCoder encodeObject:_runLoopModes forKey:CPUndoManagerRunLoopModesKey];
877  [aCoder encodeBool:_groupsByEvent forKey:CPUndoManagerGroupsByEventKey];
878 }
879 
880 @end
881 
882 @implementation _CPUndoManagerProxy : CPProxy
883 {
884  CPUndoManager _undoManager;
885 }
886 
887 - (CPMethodSignature)methodSignatureForSelector:(SEL)aSelector
888 {
889  return [_undoManager _methodSignatureOfPreparedTargetForSelector:aSelector];
890 }
891 
892 - (void)forwardInvocation:(CPInvocation)anInvocation
893 {
894  [_undoManager _forwardInvocationToPreparedTarget:anInvocation];
895 }
896 
897 @end