-
Notifications
You must be signed in to change notification settings - Fork 15
/
GCUndoManager.m
1548 lines (1084 loc) · 38.4 KB
/
GCUndoManager.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//
// GCUndoManager.m
// GCDrawKit
//
// Created by graham on 4/12/09.
// Copyright 2009-2011 Apptree.net. All rights reserved.
//
#import "GCUndoManager.h"
// this proxy object is returned by -prepareWithInvocationTarget: if GCUM_USE_PROXY is 1. This provides a similar behaviour to NSUndoManager
// on 10.6 so that a wider range of methods can be submitted as undo tasks. Unlike 10.6 however, it does not bypass um's -forwardInvocation:
// method, so subclasses still work when -forwardInvocaton: is overridden.
@interface GCUndoManagerProxy : NSProxy
{
@private
GCUndoManager* mUndoManager;
id mNextTarget;
}
- (id) initWithUndoManager:(GCUndoManager*) um;
- (void) forwardInvocation:(NSInvocation*) inv;
- (NSMethodSignature*) methodSignatureForSelector:(SEL) selector;
- (BOOL) respondsToSelector:(SEL) selector;
- (void) gcum_setTarget:(id) target;
@end
// if this is set to 0 no proxy is used and -prepareWithInvocationTarget: returns the undo manager itself.
// In general using the proxy is recommended. NSUndoManager uses a proxy on 10.6 and later, but does not on 10.5 and earlier.
#define GCUM_USE_PROXY 1
// the grouping level is maintained as groups are opened and closed. The GNUStep implementation works it out by traversing
// the tree of open groups. They should both give the same answer - you can do it using traversal by setting this to 1 if you want.
#define CALCULATE_GROUPING_LEVEL 0
#pragma mark -
@implementation GCUndoManager
- (void) beginUndoGrouping
{
// only create group if undo registration is enabled,
// otherwise groups get created when undo registration is disabled and causes GUI to say document is "Edited" on Lion.
if([self isUndoRegistrationEnabled])
{
// starts a new group. If there's an existing one open, this is nested inside it. A group must be opened before any undo tasks can be
// accumulated. If groupsByEvent is YES, a group will be automatically opened and closed around the main event loop when the first
// valid task is submitted. Unlike NSUndoManger it is safe to merely open and then close a group with no tasks submitted
// - the empty group is (optionally) removed automatically. (see -endUndoGrouping)
GCUndoGroup* newGroup = [[GCUndoGroup alloc] init];
THROW_IF_FALSE( newGroup != nil, @"unable to create new group");
if( mGroupLevel == 0 )
{
if([self isUndoing])
[self pushGroupOntoRedoStack:newGroup];
else
[self pushGroupOntoUndoStack:newGroup];
}
else
{
THROW_IF_FALSE( mOpenGroupRef != nil, @"internal inconsistency - group level was > 0 but no open group was found");
[[self currentGroup] addTask:newGroup];
}
mOpenGroupRef = newGroup;
[newGroup release];
if(![self isUndoing] && mGroupLevel > 0 )
[self checkpoint];
++mGroupLevel;
NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotificationName:NSUndoManagerDidOpenUndoGroupNotification object:self];
}
}
- (void) endUndoGrouping
{
// close the current group. If the group level is 1, this completes the top-level group. Otherwise restore the group that
// was operating when this group was opened (its parent group). If no top level group is open, does nothing.
if( mGroupLevel > 0 )
{
[self checkpoint];
--mGroupLevel;
THROW_IF_FALSE( mGroupLevel >= 0, @"group level is negative - internal inconsistency");
THROW_IF_FALSE( mOpenGroupRef != nil, @"bad group state - attempt to close a nested group with no group open");
// the value of this may change after we pop or remove empty groups, so grab it now
BOOL hadTasksBeforeEnding = [[self currentGroup] hasTask];
if( mGroupLevel == 0 )
{
// closing outer group. If it's empty, remove it. This is what NSUndoManager should do, but doesn't. That means that this
// um is easier to use in some situations such as grouping across a series of events that may or may not submit undo tasks.
// If no tasks were submitted, no bogus empty undo task remains on the stack. In addition no closure notification is sent
// so as far as the client is concerned, it just never happened.
@try
{
if([self automaticallyDiscardsEmptyGroups] && !hadTasksBeforeEnding)
{
if([self isUndoing])
[self popRedo];
else
[self popUndo];
}
else if([self undoManagerState] == kGCUndoCollectingTasks)
{
// this notification is not exactly in line with documentation, but it correctly ensures that NSDocument's change count
// management is correct. I suspect that the documentation is in error.
NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotificationName:NSUndoManagerWillCloseUndoGroupNotification object:self];
}
}
@catch( NSException* excp )
{
NSLog(@"an exception occurred while closing an undo group - ignored: %@", excp );
}
@finally
{
//NSLog(@"top level group closed: %@", mOpenGroupRef);
mOpenGroupRef = nil;
// keep the number of undo tasks at the top level limited to the undoLevels
// by discarding the oldest tasks
if([self levelsOfUndo] > 0 && !mIsRemovingTargets)
{
mIsRemovingTargets = YES;
while([self numberOfUndoActions] > [self levelsOfUndo])
[mUndoStack removeObjectAtIndex:0];
mIsRemovingTargets = NO;
}
}
}
else
{
// closing an inner nested group, so restore its containing group as the open one.
mOpenGroupRef = [[self currentGroup] parentGroup];
THROW_IF_FALSE( mOpenGroupRef != nil, @"nested group could not be restored - bad parent group ref");
}
// this notification is new for 10.7 - according to inside info, NSUndoManager only posts it while doing, not otherwise
// see: https://devforums.apple.com/thread/110036?tstart=0
// GCUndoManager sends this notification as well. This is necessary for NSDocument compatibility on 10.7, but may be used on
// earlier systems if you wish. The notification is only sent while collecting tasks, not when undoing or redoing.
// We also apply the same improvement over NSUndoManager regarding ignoring empty undo groups which is explained
// above, not posting this in the same way that we don't post NSUndoManagerWillCloseUndoGroupNotification.
// NSUndoManagerWillCloseUndoGroupNotification and NSUndoManagerDidCloseUndoGroupNotification, if posted, will
// always be posted in pairs.
if((![self automaticallyDiscardsEmptyGroups] || hadTasksBeforeEnding) && ([self undoManagerState] == kGCUndoCollectingTasks))
{
// if this action is discardable, create userInfo indicating such
GCUndoGroup* topGroup = [self peekUndo] ;
NSDictionary* userInfo = nil ;
if ([topGroup actionIsDiscardable]) {
// To explain this #if, see note following end of this method.
#if ((MAC_OS_X_VERSION_MAX_ALLOWED >= 1070) && (MAC_OS_X_VERSION_MIN_REQUIRED >= 1070))
userInfo = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:NSUndoManagerGroupIsDiscardableKey] ;
#else
userInfo = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:@"NSUndoManagerGroupIsDiscardableKey"] ;
#endif
}
NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter];
// To explain this #if, see note following end of this method.
#if ((MAC_OS_X_VERSION_MAX_ALLOWED >= 1070) && (MAC_OS_X_VERSION_MIN_REQUIRED >= 1070))
[notificationCenter postNotificationName:NSUndoManagerDidCloseUndoGroupNotification object:self userInfo:userInfo];
#else
[notificationCenter postNotificationName:@"NSUndoManagerDidCloseUndoGroupNotification" object:self userInfo:userInfo];
#endif
}
}
}
// explanation of #if in -endUndoGrouping
// we have a string constant symbol which is defined in the 10.7 SDK and used by the 10.7 runtime. this creates two problems.
// problem 1. when *compiling* in a system prior to 10.7, it won't even compile with this symbol.
// problem 2. when *running* in a system prior to 10.7, when accessing the undefined symbol, the app will crash.
// to solve problem 1, we require that (MAC_OS_X_VERSION_MAX_ALLOWED >= 1070).
// to solve problem 2, we require that (MAC_OS_X_VERSION_MIN_REQUIRED >= 1070).
// hence the && in the #if.
- (BOOL) canUndo
{
return [self numberOfUndoActions] > 0 && [self undoManagerState] == kGCUndoCollectingTasks;
}
- (BOOL) canRedo
{
[self checkpoint]; // why here? Just conforming to documentation
return [self numberOfRedoActions] > 0 && [self undoManagerState] == kGCUndoCollectingTasks;
}
- (void) undo
{
THROW_IF_FALSE([self groupingLevel] < 2, @"can't undo with a nested group open");
[self endUndoGrouping];
[self undoNestedGroup];
}
- (void) redo
{
THROW_IF_FALSE([self undoManagerState] == kGCUndoCollectingTasks, @"can't redo - already undoing or redoing");
THROW_IF_FALSE([self groupingLevel] == 0, @"can't redo - a group is still open");
[self checkpoint];
[self popRedoAndPerformTasks];
}
- (BOOL) isUndoing
{
return [self undoManagerState] == kGCUndoIsUndoing;
}
- (BOOL) isRedoing
{
return [self undoManagerState] == kGCUndoIsRedoing;
}
- (void) undoNestedGroup
{
// warning: this is not the same as NSUndoManager, but does the same as -undo except the top-level group must be closed to invoke this.
// At present the ability to undo an inner nested group while the top group is still open is not implemented.
THROW_IF_FALSE([self undoManagerState] == kGCUndoCollectingTasks, @"can't undo - already undoing or redoing");
THROW_IF_FALSE([self groupingLevel] == 0, @"can't undo - a group is still open");
[self checkpoint];
[self popUndoAndPerformTasks];
}
- (void) enableUndoRegistration
{
THROW_IF_FALSE( mEnableLevel < 0, @"inconsistent state - undo enabled when not previously disabled");
++mEnableLevel;
}
- (void) disableUndoRegistration
{
--mEnableLevel;
}
- (BOOL) isUndoRegistrationEnabled
{
return mEnableLevel >= 0;
}
- (NSInteger) groupingLevel
{
#if CALCULATE_GROUPING_LEVEL
NSInteger level = 0;
GCUndoGroup* group = [self currentGroup];
while( group )
{
++level;
group = [group parentGroup];
}
THROW_IF_FALSE( level == mGroupLevel, @"calculated group level does not match recorded level - internal inconsistency");
return level;
#else
return mGroupLevel;
#endif
}
- (BOOL) groupsByEvent
{
return mGroupsByEvent;
}
- (void) setGroupsByEvent:(BOOL) groupByEvent
{
mGroupsByEvent = groupByEvent;
}
- (NSUInteger) levelsOfUndo
{
return mLevelsOfUndo;
}
- (void) setLevelsOfUndo:(NSUInteger) levels
{
mLevelsOfUndo = levels;
// if the new levels are exceeded, trim the stacks accordingly
if( levels > 0 && !mIsRemovingTargets )
{
mIsRemovingTargets = YES;
while([self numberOfUndoActions] > levels)
[mUndoStack removeObjectAtIndex:0];
while([self numberOfRedoActions] > levels)
[mRedoStack removeObjectAtIndex:0];
mIsRemovingTargets = NO;
}
}
- (NSArray*) runLoopModes
{
return mRunLoopModes;
}
- (void) setRunLoopModes:(NSArray*) modes
{
[modes retain];
[mRunLoopModes release];
mRunLoopModes = modes;
// n.b. if this is changed while a callback is pending, the new modes won't take effect until
// the next event cycle.
}
- (void) setActionName:(NSString*) actionName
{
// for compatibility with NSUndoManager, conditionally open a group - this allows an action name to be set
// before any task is submitted. I think it's incorrect that tasks are nameable before being created and should be
// named at the end - but if someone's code does that, this allows it to work.
//[self conditionallyBeginUndoGrouping];
if([self isUndoing])
[[self peekRedo] setActionName:actionName];
else
[[self peekUndo] setActionName:actionName];
}
- (NSString*) undoActionName
{
return [[self peekUndo] actionName];
}
- (NSString*) redoActionName
{
return [[self peekRedo] actionName];
}
- (NSString*) undoMenuItemTitle
{
return [self undoMenuTitleForUndoActionName:[self undoActionName]];
}
- (NSString*) redoMenuItemTitle
{
return [self redoMenuTitleForUndoActionName:[self redoActionName]];
}
- (void)setActionIsDiscardable:(BOOL)discardable
{
if([self isUndoing])
[[self peekRedo] setActionIsDiscardable:discardable];
else
[[self peekUndo] setActionIsDiscardable:discardable];
}
- (NSString*) undoMenuTitleForUndoActionName:(NSString*) actionName
{
if([self canUndo])
{
if( actionName )
return [NSString stringWithFormat:@"%@ %@", NSLocalizedString(@"Undo", nil), actionName];
else
return NSLocalizedString(@"Undo", nil);
}
else
return NSLocalizedString(@"Nothing To Undo", nil);
}
- (NSString*) redoMenuTitleForUndoActionName:(NSString*) actionName
{
if([self canRedo])
{
if( actionName )
return [NSString stringWithFormat:@"%@ %@", NSLocalizedString(@"Redo", nil), actionName];
else
return NSLocalizedString(@"Redo", nil);
}
else
return NSLocalizedString(@"Nothing To Redo", nil);
}
- (id) prepareWithInvocationTarget:(id) target
{
// Records the target and returns either the proxy or self. The proxy allows methods also implemented by this class
// to be recorded as forward invocations, and is generally a good idea (Snow Leopard does the same, but not in
// a way that is backward compatible with overrides of -forwardInvocation: This implementation does not have that bug.
if( mProxy )
{
[mProxy gcum_setTarget:target];
return mProxy;
}
else
{
mNextTarget = target;
return self;
}
}
- (void) forwardInvocation:(NSInvocation*) invocation
{
// registers a new undo task using a forwarded invocation, called after -prepareWithInvocationTarget: If registration
// disabled, does nothing, If coalescing enabled and the previous target and selector was the same, also does nothing.
// Will open a top-level group automtically if necessary and -groupsByEvent is YES.
if([self isUndoRegistrationEnabled])
{
THROW_IF_FALSE( invocation != nil, @"-forwardInvocation: was passed an invalid nil invocation" );
GCConcreteUndoTask* task = [[[GCConcreteUndoTask alloc] initWithInvocation:invocation] autorelease];
[task setTarget:mNextTarget retained:[self retainsTargets]];
[self submitUndoTask:task];
}
mNextTarget = nil;
}
- (void) registerUndoWithTarget:(id) target selector:(SEL) selector object:(id) anObject
{
// registers a new undo task using the supplied target, selector and optional object parameter. If registration
// disabled, does nothing, If coalescing enabled and the previous target and selector was the same, also does nothing.
// Will open a top-level group automatically if necessary and -groupsByEvent is YES.
if([self isUndoRegistrationEnabled])
{
THROW_IF_FALSE( selector != NULL, @"invalid (NULL) selector passed to registerUndoWithTarget:selector:object:" );
GCConcreteUndoTask* task = [[[GCConcreteUndoTask alloc] initWithTarget:target selector:selector object:anObject] autorelease];
[task setTarget:target retained:[self retainsTargets]];
[self submitUndoTask:task];
}
mNextTarget = nil;
}
- (void) removeAllActions
{
// removes all tasks from the undo/redo stacks and puts the undo manager back into its default state, clearing any open groups
// or temporary references.
if( !mIsRemovingTargets )
{
// prevent re-entrancy, in case targets are retained and releasing them calls -removeAllActionsWithTarget:
mIsRemovingTargets = YES;
[mUndoStack removeAllObjects];
[mRedoStack removeAllObjects];
mIsRemovingTargets = NO;
[self reset];
}
}
- (void) removeAllActionsWithTarget:(id) target
{
// removes all tasks having the given target. Groups that become empty as a result are also removed.
if( !mIsRemovingTargets )
{
// prevent re-entrancy, in case targets are retained and releasing them would call this again
mIsRemovingTargets = YES;
NSArray* temp = [[self undoStack] copy];
NSEnumerator* iter = [temp objectEnumerator];
GCUndoGroup* task;
while(( task = [iter nextObject]))
{
[task removeTasksWithTarget:target undoManager:self];
// delete groups that become empty unless it's the current group
if(![task hasTask] && task != [self currentGroup])
{
[mUndoStack removeObject:task];
}
}
[temp release];
temp = [[self redoStack] copy];
iter = [temp objectEnumerator];
while(( task = [iter nextObject]))
{
[task removeTasksWithTarget:target undoManager:self];
// delete groups that become empty unless it's the current group
if(![task hasTask] && task != [self currentGroup])
{
[mRedoStack removeObject:task];
}
}
[temp release];
mIsRemovingTargets = NO;
}
mNextTarget = nil;
}
#pragma mark -
#pragma mark - private NSUndoManager API
- (void) _processEndOfEventNotification:(NSNotification*) note
{
#pragma unused(note)
// private API invoked by NSDocument before a document is saved. Does nothing, but required for NSDocument compatibility.
//NSLog(@"_processEndOfEventNotification: %@", note );
}
#pragma mark -
#pragma mark - additional API
- (void) setAutomaticallyDiscardsEmptyGroups:(BOOL) autoDiscard
{
// set whether empty groups are automatically discarded when the top level group is closed. Default is YES. Set to
// NO for NSUndoManager behaviour - could conceivably be used to trigger undo managed outside of the undo manager.
// However this behaviour is buggy for normal usage of the undo manager. Setting this from NO to YES does not
// remove existing empty groups. Used in -endUndoGrouping.
mAutoDeleteEmptyGroups = autoDiscard;
}
- (BOOL) automaticallyDiscardsEmptyGroups
{
return mAutoDeleteEmptyGroups;
}
- (void) enableUndoTaskCoalescing
{
mCoalescing = YES;
}
- (void) disableUndoTaskCoalescing
{
mCoalescing = NO;
}
- (BOOL) isUndoTaskCoalescingEnabled
{
return mCoalescing;
}
- (void) setCoalescingKind:(GCUndoTaskCoalescingKind) kind
{
// sets the behaviour for coalescing. kGCCoalesceLastTask (default) checks just the most recent task submitted, whereas
// kGCCoalesceAllMatchingTasks checks all in the current group. Last task is appropriate for property changes such as
// ABBBBBBA > ABA, where the last A needs to be included but the intermediate B's do not. The other kind is better for changes
// such as ABABABAB > AB where a repeated sequence is coalesced into a single example of the sequence.
mCoalKind = kind;
}
- (GCUndoTaskCoalescingKind) coalescingKind
{
return mCoalKind;
}
- (void) setRetainsTargets:(BOOL) retainsTargets
{
// NSUndoManager does not retain its targets. In general, that is the right thing to do, but simpler memory management can
// be obtained when targets are retained. The default is NO, and should only be set to YES if you are certain of the consequences.
// Note that existing invocations are unaffected by this being changed, only subsequent ones are.
mRetainsTargets = retainsTargets;
}
- (BOOL) retainsTargets
{
return mRetainsTargets;
}
- (void) setNextTarget:(id) target
{
// for use by the proxy only - sets the target assigned to the next created task
mNextTarget = target;
}
- (NSInteger) changeCount
{
// return the change count, which is roughly the number of individual tasks accepted. However, do not rely on the exact value,
// instead you can compare it before and after, and if it has changed, then something was added. This could be used to e.g.
// provide some additional auxiliary undoable state, such as selection changes, which are not normally considered undoable
// in their own right.
return mChangeCount;
}
- (void) resetChangeCount
{
mChangeCount = 0;
}
- (void) conditionallyBeginUndoGrouping
{
// if set to groupByEvent and no top-level group is open, this opens the group and schedules its automatic closure. Otherwise
// does nothing.
if([self groupsByEvent] && [self groupingLevel] == 0 )
{
[self beginUndoGrouping];
THROW_IF_FALSE([self groupingLevel] == 1, @"internal inconsistency - group level should be 1 here");
// schedule an automatic close of the group at the end of the event
[[NSRunLoop mainRunLoop] performSelector:@selector(endUndoGrouping) target:self argument:nil order:NSUndoCloseGroupingRunLoopOrdering modes:[self runLoopModes]];
}
}
- (GCUndoGroup*) peekUndo
{
// return the current top undo task without popping it off the stack.
// If the stack is empty, returns nil.
return [[self undoStack] lastObject];
}
- (GCUndoGroup*) peekRedo
{
// return the current top redo task without popping it off the stack
// If the stack is empty, returns nil.
return [[self redoStack] lastObject];
}
- (NSUInteger) numberOfUndoActions
{
return [[self undoStack] count];
}
- (NSUInteger) numberOfRedoActions
{
return [[self redoStack] count];
}
- (GCUndoGroup*) currentGroup
{
// return the currently open group, or nil if no group is open
return mOpenGroupRef;
}
- (NSArray*) undoStack
{
return mUndoStack;
}
- (NSArray*) redoStack
{
return mRedoStack;
}
- (void) pushGroupOntoUndoStack:(GCUndoGroup*) aGroup
{
THROW_IF_FALSE( aGroup != nil, @"invalid attempt to push a nil group onto undo stack");
[mUndoStack addObject:aGroup];
}
- (void) pushGroupOntoRedoStack:(GCUndoGroup*) aGroup
{
THROW_IF_FALSE( aGroup != nil, @"invalid attempt to push a nil group onto redo stack");
[mRedoStack addObject:aGroup];
}
- (BOOL) submitUndoTask:(GCConcreteUndoTask*) aTask
{
// during task collection, this is called to coalesce and add the task to the current group if needed. A group is opened
// if necessary and groups by Event is YES. This is invoked by -forwardInvocation: and -registerUndoWithTarget:selector:object:
// returns YES if the task was added, NO if it was not (coalesced away).
THROW_IF_FALSE( aTask != nil, @"invalid task was nil in -submitUndoTask:");
// if coalescing, reject invocation that matches an already registered target and selector within the current group.
// Coalescing is never done while redoing or undoing. Because this matches any already-registered action, not just the
// last action registered, it will also coalesce actions made up of multiple property changes. The match only checks the
// current open group, not any subgroups, so opening & closing groups automatically isolates coalescing to the current
// group scope as it should.
if([self isUndoTaskCoalescingEnabled] && ([self undoManagerState] == kGCUndoCollectingTasks) && ([self currentGroup] != nil ))
{
if([self coalescingKind] == kGCCoalesceLastTask)
{
GCConcreteUndoTask* lastTask = [[self currentGroup] lastTaskIfConcrete];
if([lastTask target] == [aTask target] && [lastTask selector] == [aTask selector])
return NO;
}
else
{
NSArray* matchingTasks = [[self currentGroup] tasksWithTarget:[aTask target] selector:[aTask selector]];
if([matchingTasks count] > 0 )
return NO;
}
}
// for just-in-time grouping, open a group now if not open already and groupsByEvent is YES
[self conditionallyBeginUndoGrouping];
THROW_IF_FALSE( mOpenGroupRef != nil, @"invalid attempt to add undo task with no open group");
// change count is bumped for all registered tasks. Clients can check this to see whether anything was actually
// submitted for undo, which can be useful when supplying auxiliary undoable state such as selection changes.
++mChangeCount;
[[self currentGroup] addTask:aTask];
//NSLog(@"new task submitted %@: %@", [self isUndoing]? @"to r-stack" : @"to u-stack", aTask );
// if not undoing or redoing, clear the redo stack (a new mainstream task)
if([self undoManagerState] == kGCUndoCollectingTasks )
[self clearRedoStack];
return YES;
}
- (void) popUndoAndPerformTasks
{
// pops the top undo group and invokes all of its tasks
if([self numberOfUndoActions] > 0 && [[self peekUndo] hasTask])
{
NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotificationName:NSUndoManagerWillUndoChangeNotification object:self];
[self setUndoManagerState:kGCUndoIsUndoing];
[self beginUndoGrouping];
// the group's targets will remain retained at least until the end of the event cycle.
GCUndoGroup* group = [self peekUndo];
//NSLog(@"------ undoing ------");
@try
{
[group perform];
}
@catch( NSException* excp )
{
NSLog(@"an exception occurred while performing Undo - undo manager will be cleaned up: %@", excp );
@throw;
}
@finally
{
// by default copy the action name to the top of the redo stack - client code might
// change it but if not at least it's set to the same name initially. Safe because this
// was called between begin/end group, and that method has added an empty group to the
// relevant stack. If no tasks were actually submitted, the group will be discarded
if([self isUndoRegistrationEnabled])
[[self peekRedo] setActionName:[group actionName]];
// done with the undo task so pop it
[self popUndo];
[self endUndoGrouping];
[self setUndoManagerState:kGCUndoCollectingTasks];
[notificationCenter postNotificationName:NSUndoManagerDidUndoChangeNotification object:self];
}
}
}
- (void) popRedoAndPerformTasks
{
// pops the top redo group and invokes all of its tasks
if(([self numberOfRedoActions] > 0) && [[self peekRedo] hasTask])
{
NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotificationName:NSUndoManagerWillRedoChangeNotification object:self];
[self setUndoManagerState:kGCUndoIsRedoing];
[self beginUndoGrouping];
GCUndoGroup* group = [self peekRedo];
//NSLog(@"------ redoing ------");
@try
{
[group perform];
}
@catch( NSException* excp )
{
NSLog(@"an exception occurred while performing Redo - undo manager will be cleaned up: %@", excp );
@throw;
}
@finally
{
// by default copy the action name to the top of the undo stack - client code might
// change it but if not at least it's set to the same name initially
if([self isUndoRegistrationEnabled])
[[self peekUndo] setActionName:[group actionName]];
[self popRedo];
[self endUndoGrouping];
[self setUndoManagerState:kGCUndoCollectingTasks];
[notificationCenter postNotificationName:NSUndoManagerDidRedoChangeNotification object:self];
}
}
}
- (GCUndoGroup*) popUndo
{
// pops the top undo task and returns it, or nil if the stack is empty.
if([mUndoStack count] > 0 )
{
GCUndoGroup* group = [[[self peekUndo] retain] autorelease];
[mUndoStack removeLastObject];
return group;
}
else
return nil;
}
- (GCUndoGroup*) popRedo
{
// pops the top redo task and returns it, or nil if the stack is empty.
if([mRedoStack count] > 0 )
{
GCUndoGroup* group = [[[self peekRedo] retain] autorelease];
[mRedoStack removeLastObject];
return group;
}
else
return nil;
}
- (void) clearRedoStack
{
// removes all objects from the redo stack
if( !mIsRemovingTargets )
{
mIsRemovingTargets = YES;
[mRedoStack removeAllObjects];
mIsRemovingTargets = NO;
}
}
- (void) checkpoint
{
// sends the checkpoint notification. Frankly, this seems very vague and called at all sorts of random points, so it's unclear
// exactly what the notification is meant to signify. The GNUStep implementation also sends it more than the current documentation
// for NSUndoManager indicates. This implementation follows the current documentation.
if( !mIsInCheckpoint )
{
mIsInCheckpoint = YES;
NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter];