-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy pathGCUndoManager.h
319 lines (234 loc) · 11.7 KB
/
GCUndoManager.h
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
//
// GCUndoManager.h
// GCDrawKit
//
// Created by graham on 4/12/09.
// Copyright 2009-2012 Apptree.net. All rights reserved.
//
// VERSION HISTORY:
// 2009/12/09 - first release
// 2010/01/01 - fixes for Core Data, optional retaining of targets, and prevention of reentrancy when removing tasks
// 2011/01/11 - fix to ensure submitting tasks in response to a checkpoint notification is correctly handled
// 2011/07/08 - added NSUndoManagerDidCloseUndoGroupNotification for 10.7 (Lion) compatibility
#import <Foundation/Foundation.h>
// internal undo manager state is one of these constants
typedef enum
{
kGCUndoCollectingTasks = 0,
kGCUndoIsUndoing = 1,
kGCUndoIsRedoing = 2
}
GCUndoManagerState;
typedef enum
{
kGCCoalesceLastTask = 0,
kGCCoalesceAllMatchingTasks = 1
}
GCUndoTaskCoalescingKind;
@class GCUndoGroup, GCUndoManagerProxy, GCConcreteUndoTask;
// the undo manager is a public-API compatible replacement for NSUndoManager but features a simpler internal implementation, some bug fixes and less
// fragility than NSUndoManager. It can be used with NSDocument's -setUndoManager: method (cast to id or NSUndoManager*). It is compatible with Core Data, traditional retain-release, and garbage collection.
@interface GCUndoManager : NSObject
{
@private
NSMutableArray* mUndoStack; // list of groups making up the undo stack
NSMutableArray* mRedoStack; // list of groups making up the redo stack
NSArray* mRunLoopModes; // current run loop modes, used by automatic grouping by event
id mNextTarget; // next prepared target
GCUndoGroup* mOpenGroupRef; // internal reference to current open group
GCUndoManagerProxy* mProxy; // the proxy object returned by -prepareWithInvocationTarget: if proxying is used
NSInteger mGroupLevel; // current grouping level, 0 = no groups open
NSUInteger mLevelsOfUndo; // how many undo actions are added before old ones are discarded, 0 = unlimited
NSInteger mEnableLevel; // enable ref count, 0 = enabled.
NSInteger mChangeCount; // count of changes (submitting any task increments this)
GCUndoManagerState mState; // current undo manager state
GCUndoTaskCoalescingKind mCoalKind; // coalescing behaviour - match on most recent task or all tasks in group
BOOL mGroupsByEvent; // YES if automatic grouping occurs for the main loop event cycle
BOOL mCoalescing; // YES if consecutive tasks are coalesced
BOOL mAutoDeleteEmptyGroups; // YES if empty groups are automatically removed from the stack
BOOL mRetainsTargets; // YES if invocation targets are retained
BOOL mIsRemovingTargets; // YES during stack clean-up to prevent re-entrancy
BOOL mIsInCheckpoint; // YES during the notification processing for a checkpoint
}
// NSUndoManager compatible API
// undo groups
- (void) beginUndoGrouping;
- (void) endUndoGrouping;
- (NSInteger) groupingLevel;
- (BOOL) groupsByEvent;
- (void) setGroupsByEvent:(BOOL) groupByEvent;
- (NSArray*) runLoopModes;
- (void) setRunLoopModes:(NSArray*) modes;
// enabling undo registration
- (void) enableUndoRegistration;
- (void) disableUndoRegistration;
- (BOOL) isUndoRegistrationEnabled;
// setting the number of undos allowed before old ones are discarded
- (NSUInteger) levelsOfUndo;
- (void) setLevelsOfUndo:(NSUInteger) levels;
// performing the undo or redo
- (BOOL) canUndo;
- (BOOL) canRedo;
- (void) undo;
- (void) redo;
- (void) undoNestedGroup;
- (BOOL) isUndoing;
- (BOOL) isRedoing;
// undo menu management
// sets the action name of the receiver's current undo group
// setting the action name should be done as a high-level "finishing off"
// work within the controller layer of the MVC layering. usually, the
// action name is set at the end of an action method (I believe it's no
// coincidence that the UM uses the term 'action name' to suggest a
// connection with an 'action method'). example:
// - (IBAction) someAction:(id) sender
// {
// [dataModel doThis]; // undoable
// [dataModel doThat]; // undoable
// [[self undoManager] setActionName:[sender title]];
// }
// Even if -doThis and -doThat set action names, these are ignored
// because the last action name set is the one that "sticks". also, in this
// example I use the sender's title as the action name, which works great for
// menu and button actions, and means that the action is automatically
// localised along with the UI. for actions triggered by other elements, a
// fixed localisable string does the job.
- (void) setActionName:(NSString*) actionName;
- (NSString*) undoActionName;
- (NSString*) redoActionName;
- (NSString*) undoMenuItemTitle;
- (NSString*) redoMenuItemTitle;
- (NSString*) undoMenuTitleForUndoActionName:(NSString*) actionName;
- (NSString*) redoMenuTitleForUndoActionName:(NSString*) actionName;
// for supporting the Versions Browser in Mac OS 10.7
- (void) setActionIsDiscardable:(BOOL) discardable;
// registering actions with the undo manager
- (id) prepareWithInvocationTarget:(id) target;
- (void) forwardInvocation:(NSInvocation*) invocation;
- (void) registerUndoWithTarget:(id) target selector:(SEL) selector object:(id) anObject;
// removing actions
- (void) removeAllActions;
- (void) removeAllActionsWithTarget:(id) target;
// private NSUndoManager API for compatibility
- (void) _processEndOfEventNotification:(NSNotification*) note;
#pragma mark -
// additional API
// automatic empty group discarding (default = YES)
- (void) setAutomaticallyDiscardsEmptyGroups:(BOOL) autoDiscard;
- (BOOL) automaticallyDiscardsEmptyGroups;
// task coalescing (default = NO)
- (void) enableUndoTaskCoalescing;
- (void) disableUndoTaskCoalescing;
- (BOOL) isUndoTaskCoalescingEnabled;
- (void) setCoalescingKind:(GCUndoTaskCoalescingKind) kind;
- (GCUndoTaskCoalescingKind) coalescingKind;
// retaining targets (default = NO)
- (void) setRetainsTargets:(BOOL) retainsTargets;
- (BOOL) retainsTargets;
- (void) setNextTarget:(id) target;
// getting/resetting change count
- (NSInteger) changeCount;
- (void) resetChangeCount;
#pragma mark -
// internal methods - public to permit overriding
- (GCUndoGroup*) currentGroup;
- (NSArray*) undoStack;
- (NSArray*) redoStack;
- (GCUndoGroup*) peekUndo;
- (GCUndoGroup*) peekRedo;
- (NSUInteger) numberOfUndoActions;
- (NSUInteger) numberOfRedoActions;
- (void) pushGroupOntoUndoStack:(GCUndoGroup*) aGroup;
- (void) pushGroupOntoRedoStack:(GCUndoGroup*) aGroup;
- (BOOL) submitUndoTask:(GCConcreteUndoTask*) aTask;
- (void) popUndoAndPerformTasks;
- (void) popRedoAndPerformTasks;
- (GCUndoGroup*) popUndo;
- (GCUndoGroup*) popRedo;
- (void) clearRedoStack;
- (void) checkpoint;
- (GCUndoManagerState) undoManagerState;
- (void) setUndoManagerState:(GCUndoManagerState) aState;
- (void) reset;
- (void) conditionallyBeginUndoGrouping;
// debugging utility:
- (void) explodeTopUndoAction;
@end
#pragma mark -
// undo tasks (actions) come in two types - groups and concrete tasks. Both descend from the same semi-abstract base which
// provides the 'back pointer' to the parent group. The -perform method must be overridden by concrete subclasses.
@interface GCUndoTask : NSObject
{
@private
GCUndoGroup* mGroupRef;
}
- (GCUndoGroup*) parentGroup;
- (void) setParentGroup:(GCUndoGroup*) parent;
- (void) perform;
@end
#pragma mark -
// undo groups can contain any number of other groups or concrete tasks. The top level actions in the undo/redo stacks always consist
// of groups, even if they only contain a single concrete task. The group also provides the storage for the action name associated with
// the action, and whether or not the group is discardable. Groups own their tasks.
@interface GCUndoGroup : GCUndoTask
{
@private
NSString* mActionName;
NSMutableArray* mTasks;
BOOL mActionIsDiscardable;
}
- (void) addTask:(GCUndoTask*) aTask;
- (GCUndoTask*) taskAtIndex:(NSUInteger) indx;
- (GCConcreteUndoTask*) lastTaskIfConcrete;
- (NSArray*) tasks;
- (NSArray*) tasksWithTarget:(id) target selector:(SEL) selector;
- (BOOL) hasTask;
- (void) removeTasksWithTarget:(id) aTarget undoManager:(GCUndoManager*) um;
- (void) setActionName:(NSString*) name;
- (NSString*) actionName;
// set the latest undo action to discardable if it may be safely discarded when a document can not be saved for any reason.
// an example might be an undo action that changes the viewable area of a document. To find out if an undo group contains only
// discardable actions, look for the NSUndoManagerGroupIsDiscardableKey in the userInfo dictionary of the
// NSUndoManagerDidCloseUndoGroupNotification.
- (void) setActionIsDiscardable:(BOOL) discardable;
- (BOOL) actionIsDiscardable;
@end
#pragma mark -
// concrete tasks wrap the NSInvocation which embodies the actual method call that is made when an action is undone or redone.
// Concrete tasks own the invocation, which is set to optionally retain its target and arguments.
@interface GCConcreteUndoTask : GCUndoTask
{
@private
NSInvocation* mInvocation;
id mTarget;
BOOL mTargetRetained;
}
- (id) initWithInvocation:(NSInvocation*) inv;
- (id) initWithTarget:(id) target selector:(SEL) selector object:(id) object;
- (void) setTarget:(id) target retained:(BOOL) retainIt;
- (id) target;
- (SEL) selector;
@end
// macros to throw exceptions (similar to NSAssert but always compiled in)
#ifndef THROW_IF_FALSE
#define THROW_IF_FALSE( condition, string ) if(!(condition)){[NSException raise:NSInternalInconsistencyException format:(string)];}
#define THROW_IF_FALSE1( condition, string, param1 ) if(!(condition)){[NSException raise:NSInternalInconsistencyException format:(string), (param1)];}
#define THROW_IF_FALSE2( condition, string, param1, param2 ) if(!(condition)){[NSException raise:NSInternalInconsistencyException format:(string), (param1), (param2)];}
#endif
/*
This class is a public API-compatible replacement for NSUndoManager. It can only be used with AppKit however, not with other types of executables.
The point of this is to provide an undo manager whose source is openly readable, available and debuggable. It also does not exhibit the
NSUndoManager bug whereby opening and closing a group without adding any tasks creates an empty task. That substantially simplifies how
it can be used in an interactive situation such as handling the mouse down/drag/up triplet of events.
This also includes task coalescing whereby consecutive tasks having the same target and selector are only submitted to the stack once. This
helps a lot with interactive tasks involving multiple events such as mouse dragging, so that undo does not replay all the intermediate steps.
Instances of this can be used as well as NSUndoManager if required. This handles all of its own event loop observing and automatic open
and close of groups independently of the standard mechanism.
Otherwise this should behave identically to NSUndoManager when used in an application, except as noted below.
The sending of notifications is not quite as it appears to be documented for NSUndoManager. If you implement as documented, the
change count for NSDocument is not managed correctly. Instead, this sends notifications in a manner that appears to be what NSUndoManager
actually does, and so NSDocument change counts work as they should. Also, the purpose and exact usage of NSCheckPointNotification is
unclear so while this follows the documentation, any code relying on this vague notification might not work correctly.
-undoNestedGroup only operates on top level groups in this implementation, and is thus functionally equivalent to -undo. In fact -undo simply
calls -undoNestedGroup here.
*/