diff --git a/GCUndoManager.h b/GCUndoManager.h index b33e362..5d44951 100644 --- a/GCUndoManager.h +++ b/GCUndoManager.h @@ -11,6 +11,7 @@ // 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 +// 2013/10/14 - implemented ARC compatibility #import @@ -272,8 +273,8 @@ GCUndoTaskCoalescingKind; { @private NSInvocation* mInvocation; - id mTarget; - BOOL mTargetRetained; + __weak id mWeakTarget; + __strong id mStrongTarget; } - (id) initWithInvocation:(NSInvocation*) inv; diff --git a/GCUndoManager.m b/GCUndoManager.m index 4671526..97ca049 100644 --- a/GCUndoManager.m +++ b/GCUndoManager.m @@ -8,6 +8,8 @@ #import "GCUndoManager.h" +#import "JXArcCompatibilityMacros.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. @@ -74,7 +76,7 @@ - (void) beginUndoGrouping } mOpenGroupRef = newGroup; - [newGroup release]; + JX_RELEASE(newGroup); if(![self isUndoing] && mGroupLevel > 0 ) [self checkpoint]; @@ -367,8 +369,8 @@ - (NSArray*) runLoopModes - (void) setRunLoopModes:(NSArray*) modes { - [modes retain]; - [mRunLoopModes release]; + JX_RETAIN(modes); + JX_RELEASE(mRunLoopModes); mRunLoopModes = modes; // n.b. if this is changed while a callback is pending, the new modes won't take effect until @@ -491,7 +493,7 @@ - (void) forwardInvocation:(NSInvocation*) invocation { THROW_IF_FALSE( invocation != nil, @"-forwardInvocation: was passed an invalid nil invocation" ); - GCConcreteUndoTask* task = [[[GCConcreteUndoTask alloc] initWithInvocation:invocation] autorelease]; + GCConcreteUndoTask* task = JX_AUTORELEASE([[GCConcreteUndoTask alloc] initWithInvocation:invocation]); [task setTarget:mNextTarget retained:[self retainsTargets]]; [self submitUndoTask:task]; } @@ -510,7 +512,7 @@ - (void) registerUndoWithTarget:(id) target selector:(SEL) selector object:(i { THROW_IF_FALSE( selector != NULL, @"invalid (NULL) selector passed to registerUndoWithTarget:selector:object:" ); - GCConcreteUndoTask* task = [[[GCConcreteUndoTask alloc] initWithTarget:target selector:selector object:anObject] autorelease]; + GCConcreteUndoTask* task = JX_AUTORELEASE([[GCConcreteUndoTask alloc] initWithTarget:target selector:selector object:anObject]); [task setTarget:target retained:[self retainsTargets]]; [self submitUndoTask:task]; } @@ -563,7 +565,7 @@ - (void) removeAllActionsWithTarget:(id) target } } - [temp release]; + JX_RELEASE(temp); temp = [[self redoStack] copy]; iter = [temp objectEnumerator]; @@ -580,7 +582,7 @@ - (void) removeAllActionsWithTarget:(id) target } } - [temp release]; + JX_RELEASE(temp); mIsRemovingTargets = NO; } @@ -949,7 +951,7 @@ - (GCUndoGroup*) popUndo if([mUndoStack count] > 0 ) { - GCUndoGroup* group = [[[self peekUndo] retain] autorelease]; + GCUndoGroup* group = JX_RETAIN(JX_AUTORELEASE([self peekUndo])); [mUndoStack removeLastObject]; return group; @@ -965,7 +967,7 @@ - (GCUndoGroup*) popRedo if([mRedoStack count] > 0 ) { - GCUndoGroup* group = [[[self peekRedo] retain] autorelease]; + GCUndoGroup* group = JX_RETAIN(JX_AUTORELEASE([self peekRedo])); [mRedoStack removeLastObject]; return group; @@ -1063,7 +1065,7 @@ - (void) explodeTopUndoAction [newTaskGroup setActionName:[NSString stringWithFormat:@"%@ (%lu: %@)", [topGroup actionName], (unsigned long)++suffix, selString ]]; [self pushGroupOntoUndoStack:newTaskGroup]; - [newTaskGroup release]; + JX_RELEASE(newTaskGroup); } } } @@ -1080,7 +1082,7 @@ - (id) init mRedoStack = [[NSMutableArray alloc] init]; mGroupsByEvent = YES; - mRunLoopModes = [[NSArray arrayWithObject:NSDefaultRunLoopMode] retain]; + mRunLoopModes = JX_RETAIN([NSArray arrayWithObject:NSDefaultRunLoopMode]); mAutoDeleteEmptyGroups = YES; mCoalKind = kGCCoalesceLastTask; @@ -1098,11 +1100,13 @@ - (void) dealloc { [[NSRunLoop mainRunLoop] cancelPerformSelectorsWithTarget:self]; - [mUndoStack release]; - [mRedoStack release]; - [mRunLoopModes release]; - [mProxy release]; +#if (JX_HAS_ARC == 0) + JX_RELEASE(mUndoStack); + JX_RELEASE(mRedoStack); + JX_RELEASE(mRunLoopModes); + JX_RELEASE(mProxy); [super dealloc]; +#endif } @@ -1284,7 +1288,7 @@ - (void) removeTasksWithTarget:(id) aTarget undoManager:(GCUndoManager*) um } } - [temp release]; + JX_RELEASE(temp); } @@ -1293,8 +1297,8 @@ - (void) setActionName:(NSString*) name { // sets the group's action name. In general this is automatically handled by the owning undo manager - [name retain]; - [mActionName release]; + JX_RETAIN(name); + JX_RELEASE(mActionName); mActionName = name; } @@ -1352,14 +1356,16 @@ - (id) init +#if (JX_HAS_ARC == 0) - (void) dealloc { //NSLog(@"deallocating undo group %@", self ); - [mTasks release]; - [mActionName release]; + JX_RELEASE(mTasks); + JX_RELEASE(mActionName); [super dealloc]; } +#endif - (NSString*) description @@ -1387,14 +1393,14 @@ - (id) initWithInvocation:(NSInvocation*) inv // the invocation retains its arguments and target if the target is set at this point. Therefore the target // is set as nil and is managed independently. mTarget is set to the invocation's original target if set. - mTarget = [inv target]; + mWeakTarget = [inv target]; [inv setTarget:nil]; [inv retainArguments]; - mInvocation = [inv retain]; + mInvocation = JX_RETAIN(inv); } else { - [self autorelease]; + JX_AUTORELEASE(self); self = nil; } } @@ -1414,10 +1420,16 @@ - (id) initWithTarget:(id) target selector:(SEL) selector object:(id) object [inv setSelector:selector]; +#if JX_HAS_ARC + __unsafe_unretained +#endif + id tempObject = object; + // don't set the argument if the selector doesn't take one - if([sig numberOfArguments] >= 3 ) - [inv setArgument:&object atIndex:2]; + if([sig numberOfArguments] >= 3 ) { + [inv setArgument:&tempObject atIndex:2]; + } self = [self initWithInvocation:inv]; @@ -1425,7 +1437,7 @@ - (id) initWithTarget:(id) target selector:(SEL) selector object:(id) object // The invocation's internal target is nil. The target is not retained unless -setTarget:retained: is called with YES for . if( self ) - mTarget = target; + mWeakTarget = target; return self; } @@ -1435,20 +1447,22 @@ - (void) setTarget:(id) target retained:(BOOL) retainIt { // sets the invocation's target, optionally retaining it. - if( retainIt ) - [target retain]; + JX_RELEASE(mStrongTarget); - if( mTargetRetained ) - [mTarget release]; + if( retainIt ) { + mStrongTarget = JX_RETAIN(target); + } + else { + mStrongTarget = nil; + } - mTarget = target; - mTargetRetained = retainIt; + mWeakTarget = target; } - (id) target { - return mTarget; + return mWeakTarget; } @@ -1467,8 +1481,8 @@ - (void) perform //NSLog(@"about to invoke task %@", self ); - if( mTarget ) - [mInvocation invokeWithTarget:mTarget]; + if( mWeakTarget ) + [mInvocation invokeWithTarget:mWeakTarget]; } @@ -1478,22 +1492,22 @@ - (void) perform - (id) init { - [self autorelease]; + JX_AUTORELEASE(self); return nil; } +#if (JX_HAS_ARC == 0) - (void) dealloc { - [mInvocation release]; + JX_RELEASE(mInvocation); - if( mTargetRetained ) - [mTarget release]; + JX_RELEASE(mStrongTarget); [super dealloc]; } - +#endif - (NSString*) description { diff --git a/GCUndoManagerTestbed.xcodeproj/project.pbxproj b/GCUndoManagerTestbed.xcodeproj/project.pbxproj index 0913152..26c3871 100644 --- a/GCUndoManagerTestbed.xcodeproj/project.pbxproj +++ b/GCUndoManagerTestbed.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 45; + objectVersion = 46; objects = { /* Begin PBXBuildFile section */ @@ -31,6 +31,7 @@ 2A37F4BAFDCFA73011CA2CEA /* English */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = English; path = English.lproj/Credits.rtf; sourceTree = ""; }; 2A37F4C4FDCFA73011CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; 2A37F4C5FDCFA73011CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; + 3DF8E6E5180C447B008E3D1F /* JXArcCompatibilityMacros.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JXArcCompatibilityMacros.h; sourceTree = ""; }; 8D15AC360486D014006FF6A4 /* GCUndoManagerTestbed-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GCUndoManagerTestbed-Info.plist"; sourceTree = ""; }; 8D15AC370486D014006FF6A4 /* GCUndoManagerTestbed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GCUndoManagerTestbed.app; sourceTree = BUILT_PRODUCTS_DIR; }; BF27961E10CF43F10031B9BF /* GCUndoManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCUndoManager.h; sourceTree = ""; }; @@ -92,6 +93,7 @@ 2A37F4ABFDCFA73011CA2CEA /* Classes */ = { isa = PBXGroup; children = ( + 3DF8E6E5180C447B008E3D1F /* JXArcCompatibilityMacros.h */, 2A37F4AEFDCFA73011CA2CEA /* MyDocument.h */, 2A37F4ACFDCFA73011CA2CEA /* MyDocument.m */, BF27961E10CF43F10031B9BF /* GCUndoManager.h */, @@ -158,8 +160,11 @@ /* Begin PBXProject section */ 2A37F4A9FDCFA73011CA2CEA /* Project object */ = { isa = PBXProject; + attributes = { + LastUpgradeCheck = 0500; + }; buildConfigurationList = C05733CB08A9546B00998B17 /* Build configuration list for PBXProject "GCUndoManagerTestbed" */; - compatibilityVersion = "Xcode 3.1"; + compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 1; knownRegions = ( @@ -241,10 +246,67 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 3D80E49B18327BA400005583 /* Debug-ARC */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_ARC = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_GLOBAL_CONSTRUCTORS = YES; + GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_EFFECTIVE_CPLUSPLUS_VIOLATIONS = YES; + GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES; + GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_MISSING_PARENTHESES = YES; + GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; + GCC_WARN_PEDANTIC = YES; + GCC_WARN_SHADOW = YES; + GCC_WARN_SIGN_COMPARE = YES; + GCC_WARN_STRICT_SELECTOR_MATCH = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNKNOWN_PRAGMAS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_PARAMETER = YES; + GCC_WARN_UNUSED_VALUE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.7; + ONLY_ACTIVE_ARCH = YES; + PREBINDING = NO; + WARNING_CFLAGS = ( + "-Wall", + "-Wextra", + "-fdiagnostics-show-option", + ); + }; + name = "Debug-ARC"; + }; + 3D80E49C18327BA400005583 /* Debug-ARC */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = GCUndoManagerTestbed_Prefix.pch; + INFOPLIST_FILE = "GCUndoManagerTestbed-Info.plist"; + PRODUCT_NAME = GCUndoManagerTestbed; + }; + name = "Debug-ARC"; + }; C05733C808A9546B00998B17 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; GCC_ENABLE_FIX_AND_CONTINUE = YES; GCC_MODEL_TUNING = G5; @@ -260,6 +322,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + COMBINE_HIDPI_IMAGES = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GCC_MODEL_TUNING = G5; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -297,6 +360,7 @@ GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VALUE = YES; GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.5; ONLY_ACTIVE_ARCH = YES; PREBINDING = NO; WARNING_CFLAGS = ( @@ -335,6 +399,7 @@ GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VALUE = YES; GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.5; PREBINDING = NO; WARNING_CFLAGS = ( "-Wall", @@ -351,6 +416,7 @@ isa = XCConfigurationList; buildConfigurations = ( C05733C808A9546B00998B17 /* Debug */, + 3D80E49C18327BA400005583 /* Debug-ARC */, C05733C908A9546B00998B17 /* Release */, ); defaultConfigurationIsVisible = 0; @@ -360,6 +426,7 @@ isa = XCConfigurationList; buildConfigurations = ( C05733CC08A9546B00998B17 /* Debug */, + 3D80E49B18327BA400005583 /* Debug-ARC */, C05733CD08A9546B00998B17 /* Release */, ); defaultConfigurationIsVisible = 0; diff --git a/GCUndoTestView.h b/GCUndoTestView.h index 6891c42..286a342 100644 --- a/GCUndoTestView.h +++ b/GCUndoTestView.h @@ -8,6 +8,8 @@ #import +#import "JXArcCompatibilityMacros.h" + // n.b for simplicty this includes a very crude data model of a draggable/resizeable box. This is not a good example of MVC separation, as // the purpose of it is merely to allow testing of the undo manager code. diff --git a/GCUndoTestView.m b/GCUndoTestView.m index 03c9bff..4e5d165 100644 --- a/GCUndoTestView.m +++ b/GCUndoTestView.m @@ -17,7 +17,7 @@ - (id) initWithFrame:(NSRect) frame if (self) { draggedBox = NSMakeRect( 100, 100, 100, 100 ); - dragBoxColour = [[NSColor redColor] retain]; + dragBoxColour = JX_RETAIN([NSColor redColor]); } return self; @@ -139,8 +139,8 @@ - (void) setDraggedBoxColour:(NSColor*) aColour { [[[self undoManager] prepareWithInvocationTarget:self] setDraggedBoxColour:[self draggedBoxColour]]; - [aColour retain]; - [dragBoxColour release]; + JX_RETAIN(aColour); + JX_RELEASE(dragBoxColour); dragBoxColour = aColour; [self setNeedsDisplayInRect:draggedBox]; diff --git a/JXArcCompatibilityMacros.h b/JXArcCompatibilityMacros.h new file mode 100644 index 0000000..0f09f99 --- /dev/null +++ b/JXArcCompatibilityMacros.h @@ -0,0 +1,76 @@ +// +// JXArcCompatibilityMacros.h +// +// Created by Jan on 29.09.12. +// Copyright 2012 Jan Weiß +// +// Based on “DDMathParser.h” by Dave DeLong. +// +// Released under the BSD software licence. +// + +#ifndef JXArcCompatibilityMacros_h +#define JXArcCompatibilityMacros_h + +#ifdef __clang__ +#define JX_STRONG strong +#else +#define JX_STRONG retain +#endif + +/* +Porting help (pretty crude, could use improvement): +\[(.+) retain\] JX_RETAIN(\1) +\[(.+) release\] JX_RELEASE(\1) +\[(.+) autorelease\] JX_AUTORELEASE(\1) + +\(id\)([\w\d.]+|\[.+\]) JX_BRIDGED_CAST(id, \1) + +\(__bridge ((CF|NS)\w+(\ \*)?)\)(\w+) JX_BRIDGED_CAST(\1, \4) + + The above have usual problems with nesting. Don’t use them with “Replace all”! +*/ + +#if __has_feature(objc_arc) + +#define JX_HAS_ARC 1 +#define JX_RETAIN(_o) (_o) +#define JX_RELEASE(_o) +#define JX_AUTORELEASE(_o) (_o) + +#define JX_BRIDGED_CAST(_type, _o) (__bridge _type)(_o) +#define JX_TRANSFER_OBJC_TO_CF(_type, _o) (__bridge_retained _type)(_o) +#define JX_TRANSFER_CF_TO_OBJC(_type, _o) (__bridge_transfer _type)(_o) + +#else + +#define JX_HAS_ARC 0 +#define JX_RETAIN(_o) [(_o) retain] +#define JX_RELEASE(_o) [(_o) release] +#define JX_AUTORELEASE(_o) [(_o) autorelease] + +#define JX_BRIDGED_CAST(_type, _o) (_type)(_o) +#define JX_TRANSFER_OBJC_TO_CF(_type, _o) (_type)((_o) ? CFRetain((CFTypeRef)(_o)) : NULL) +#define JX_TRANSFER_CF_TO_OBJC(_type, _o) [(_type)CFMakeCollectable(_o) autorelease] + +#endif + + +#ifdef __clang__ + +#define JX_NEW_AUTORELEASE_POOL_WITH_NAME(_o) @autoreleasepool { +#define JX_END_AUTORELEASE_POOL_WITH_NAME(_o) } + +#define JX_DRAIN_AUTORELEASE_POOL_WITH_NAME(_o) + +#else + +#define JX_NEW_AUTORELEASE_POOL_WITH_NAME(_o) NSAutoreleasePool *(_o) = [NSAutoreleasePool new]; +#define JX_END_AUTORELEASE_POOL_WITH_NAME(_o) [(_o) drain]; + +#define JX_DRAIN_AUTORELEASE_POOL_WITH_NAME(_o) [(_o) drain] + +#endif + + +#endif diff --git a/MyDocument.h b/MyDocument.h index d049c28..5c2922d 100644 --- a/MyDocument.h +++ b/MyDocument.h @@ -9,6 +9,8 @@ #import +#import "JXArcCompatibilityMacros.h" + @class GCUndoTestView; diff --git a/MyDocument.m b/MyDocument.m index b9a1513..899b255 100644 --- a/MyDocument.m +++ b/MyDocument.m @@ -24,7 +24,7 @@ - (id)init [um enableUndoTaskCoalescing]; [um setLevelsOfUndo:16]; [self setUndoManager:(id)um]; - [um release]; + JX_RELEASE(um); [self setHasUndoManager:YES]; } return self; @@ -73,7 +73,7 @@ - (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError * - (IBAction) enableUndoAction:(id) sender { - if([sender state] == NSOnState ) + if([(NSButton *)sender state] == NSOnState ) [[self undoManager] enableUndoRegistration]; else [[self undoManager] disableUndoRegistration]; @@ -82,7 +82,7 @@ - (IBAction) enableUndoAction:(id) sender - (IBAction) enableCoalescingAction:(id) sender { - if([sender state] == NSOnState ) + if([(NSButton *)sender state] == NSOnState ) [(GCUndoManager*)[self undoManager] enableUndoTaskCoalescing]; else [(GCUndoManager*)[self undoManager] disableUndoTaskCoalescing]; @@ -91,7 +91,7 @@ - (IBAction) enableCoalescingAction:(id) sender - (IBAction) enableEventGroupingAction:(id) sender { - [[self undoManager] setGroupsByEvent:[sender state]]; + [[self undoManager] setGroupsByEvent:[(NSButton *)sender state]]; } @@ -130,7 +130,7 @@ - (IBAction) umTypeAction:(id) sender [um setLevelsOfUndo:16]; [self setUndoManager:um]; - [um release]; + JX_RELEASE(um); } diff --git a/README.md b/README.md new file mode 100644 index 0000000..b40836e --- /dev/null +++ b/README.md @@ -0,0 +1,216 @@ +GCUndoManager +============= + +## A better Undo manager + +Cocoa's built-in Undo is a great component of the framework. Those of us +who have written apps on the Mac under the classic toolbox might recall +just how difficult writing a really good Undo system was, back in the +bad old days. Having the framework take care of the overall management +of Undo with a simple way to register actions with it is a genuinely +pleasant surprise and advantage to Cocoa. + +Unfortunately, while for most people NSUndoManager will be more than +adequate, in some kinds of applications it can be somewhat awkward to +use, and when things get complicated or misbehave, the inability to "get +inside" the undo manager black-box to assist with debugging is +problematic. Most Cocoa programmers will run into the dreaded 'Undo +Manager is in an invalid state' messages logged to the console sooner or +later, which, while indicative of a programming error in client code, +can make the entire app cease to remain undoable, as recovering from +such problems appears to be difficult. In addition, NSUndoManager has +some, shall we say, quirks, which make its use in some situations harder +than it might be. When you consider that a well-written app should +implement undo pervasively, it's important that Undo is robust and +reliable, as every part of the app will be affected by it. + +The alternative undo manager presented here addresses a number of +concerns with NSUndoManager, while remaining extremely compatible with +it for most normal application use. + +## Multiple event handling + +One situation where NSUndoManager can be awkward is when dealing with +undoing changes that are the result of a stream of events, rather than +fully handled within a single event. This commonly arises when dragging +is used as means of editing the data model. Dragging breaks down into a +mouse down event, a series of drag events (including the possibility of +no event occurring), and a final mouse up event. On the face of it +NSUndoManager is equipped to deal with this - just open a group at the +start, accumulate changes, then close the group at the end. While that +does work, it has two problems. The first is that the case where no drag +event is sent is not detected, and NSUndoManager still goes ahead and +adds a new, empty Undo action to the stack, resulting in an Undo menu +item that does nothing when chosen. Users tend to consider the 'does +nothing' Undo item a bug, and quite rightly. Unfortunately they'll blame +your app, not Apple's framework, but in any case, proper behaviour is +what we want, not shifting of blame. The second problem is that even +when drag events are sent, all changes arising from each event are +faithfully recorded. That means that when Undoing, you're effectively +'replaying' all of the drag events one by one. In most cases this replay +occurs very quickly and is hardly noticeable, but it represents a waste +of time and memory, since in the vast majority of cases, you'll want to +Undo the entire drag, so only the state at the start of the drag is of +any interest. While the responsibility for this could rest with the +application, depending on its design, putting the responsibility for +handling that into the Undo manager can greatly simplify things, because +it is already aware of how tasks are being grouped, and can readily +coalesce a series of individual actions within a group as needed. +GCUndoManager supports task coalescing if required. + +In addition, NSUndoManager strictly requires that groups are carefully +balanced, and the responsibility to ensure this is with the client code. +If an imbalance arises, NSUndoManager effectively shuts down and ceases +to record or replay Undo events. Given the need to take such care, +workarounds for the empty Undo bug become needlessly complicated and +ugly. The problem is made harder by the automatic grouping by event that +NSUndoManager does, which must also be taken into account. A further +problem where invoking -endUndoGrouping appears not to actually close +the group under some circumstances, but leaves the groupingLevel at 1 +was the final motivation for this class, because it was impossible to +examine the internal state of NSUndoManager in the debugger to find out +just what the problem was. GCUndoManager still expects you to make a +reasonable effort to maintain correct group nesting - for every open +there should be a close, but is much less precious about it. For +example, you can "over close" a group harmlessly - attempts to close an +already closed top-level group are merely ignored, instead of leading to +an unrecoverable internal state. The general state is also easily reset +if things get hopelessly out-of-kilter, so it makes it easy for your app +to recover from Undo related bugs. It's surely better to 'limit the +damage' of a programming error in one part of your app and have the rest +remain working, rather than have Undo fail across the board. +GCUndoManager has no secrets - it's a straightforward implementation +with no private API and as debuggable as any other part of your app. +When Undo gets complicated, the ability to see exactly what is stored +and to directly examine its state can be a real benefit for debugging +your app. + +GCUndoManager is compatible with the public API of NSUndoManager, but it +is not a subclass of NSUndoManager (it inherits from NSObject). It can +be used with document and non-document based applications in exactly the +same way as NSUndoManager, and as far as possible it conforms to the +current documentation for NSUndoManager. Where there are differences +from the documentation, the source operates to follow NSUndoManager by +example. For instance, when used with NSDocument, the document +subscribes to notifications from the Undo manager to maintain its +'dirty' status correctly. GCUndoManager sends the same notifications in +the same places as NSUndoManager (which slightly differs from +documentation) so that the dirty state is correctly maintained. No +modifications to NSDocument are required. + +## Using the class + +The project presented here includes the undo manager and a minimal +application that tests and demonstrates its use. The undo manager is +instantiated as part of the document subclass's -init method, and passed +to its `-setUndoManager:` method, suitably cast. The test application +allows you to switch between NSUndoManager and GCUndoManager to compare +differences. The data model is a very simple one consisting of a single +draggable rectangle with three properties - location, size and colour. +All are undoable. For simplicity the 'data model' is implemented +directly by the view, and this should not be taken as a good example of +MVC design. A direct comparison of the two undo managers with respect to +the empty Undo item bug can be made - merely clicking but not dragging +the object will trigger the bug with NSUndoManager but not with +GCUndoManager. + +## Task Coalescing + +GCUndoManager supports task coalescing, where a series of identical +tasks within a group are collapsed to a single task. This can be +disabled and is not enabled by default. There are two coalescing +approaches available, set using -setCoalescingKind: The first +`kGCCoalesceLastTask` is just to discard tasks based on the most recent +one accepted. This is good for property changes consisting only of a +single property, for example an object's location, that is repeatedly +changed by a drag. Thus task sequences are coalesced as follows: + +- AAAAAA \> A +- ABBBBB \> AB +- ABBBBA \> ABA + +However, where property changes do not consist of a single property +change per drag event, but have several parts, the simple coalescing +behaviour will not be able to help, as: + +- ABABABAB \> ABABABAB + +For this kind of sequence, the second coalescing kind +`kGCCoalesceAllMatchingTasks` could be used. This coalesces tasks based on +the presence of any match within the group, not just the last one. This +results in the following behaviour: + +- ABABABAB \> AB +- ABCABCABC \> ABC +- but ABBBBBA \> AB + +The last example shows that this mode is not as general purpose as the +first. Applications can set the coalescing mode as they wish depending +on how property changes are made during a repeated sequence. Or they can +leave it in the first mode and incur some inefficiency for the second +example cases. Note that coalescing is always performed with respect to +the current open group, so can be 'restarted' by opening a subgroup. + +## Implementation details + +Unlike NSUndoManager, GCUndoManager is not a 'black box'. Internally, it +represents the recorded actions using two kinds of object, GCUndoGroup +and GCConcreteUndoTask. Both are subclasses of the semi-abstract +GCUndoTask class. GCConcreteUndoTask further stores the actual data +change as an NSInvocation which it retains. In turn the invocation +retains its arguments and target, because GCUndoManager calls its +`-retainArguments` method. Groups can be nested to any depth as +with NSUndoManager. There are no special marker or sentinel objects used +to de-mark the start and end of groups, everything is stored and managed +as a straightforward tree. The Undo and Redo stacks themselves are +NSMutableArray instances, and a group stores its contents also using a +NSMutableArray. Like the NSUndoManager in 10.6, GCUndoManager uses a +proxy object based on NSProxy that is returned by +`-prepareWithInvocationTarget:`. The proxy prevents the situation where a +property defined by the undo manager itself can't be recorded because +the undo manager will not forward methods it already responds to. While +the use of the proxy can be conditionally compiled out, it is +recommended and will work on any version of Mac OS. No API is private +and internal operations are well factored to permit overriding anywhere +that makes sense. You can peek at the current undo and redo tasks, get +the stacks themselves, pop the tasks with and without invoking them, and +many other things. Also for assistance with debugging complex undo +groups consisting of a series of individual tasks, -explodeTopUndoAction +will 'unpack' the current top-level group on the Undo stack into +separate tasks which can be individually undone. + +When a top level group is opened, it is immediately pushed onto the +relevant stack. The data member 'mOpenGroupRef' tracks the currently +open group, which might be nested within another if it is not a +top-level group. All task recording is done with reference to this +group. If the top-level group is empty when the top-level is closed, the +empty group is popped and discarded, which addresses the empty group +bug. Note that this automatic removal can be disabled - for applications +that do not submit tasks to the undo manager but merely subscribe to +notifications and manage their own undo stacks, disabling this would be +appropriate. However, in that case replacing NSUndoManager may not be +worthwhile. It is because GCUndoManager adds a top-level group to the +stack when the group is opened rather than when it is closed that it is +able to be far less finicky about strict balance, and makes recovery +from an imbalance much easier. + +## Update, 1/1/2009 + +Updated GCUndoManager has now been tested with Core Data and has been +found to work correctly, after some minor tweaks. This version changes +its memory management policy for retaining of tasks' targets: as per +NSUndoManager and general rules, GCUndoManager no longer retains its +targets by default. The undo manager should not hold stale targets, +because `-removeAllActionsWithTarget:` is required to be called whenever +any such targets are deallocated. However, for some designs retaining +targets may simplify the use of the undo manager quite considerably, so +you can now opt-in to this behaviour using `-setRetainsTargets:` passing an +argument of YES. When targets are retained, clearing the task stacks +must avoid re-entrancy, and GCUndoManager now includes a simple lock to +ensure that. + +## Update, 20/7/2011 + +Updated to include the new notification used by NSDocument in Lion +(10.7). The source is now hosted on Github, so any further changes will +be made to the repository there.