Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    text
    copied!<pre><code>// This looked like the best thread to post this. // This is incomplete skeleton example of NSDocument/NSUndoManager usage. // Use at your own risk // Ultimately, would love if Apple reviewed/corrected this and added correct sample code to their docs. // I put pseudo code in implementation specific spots: // "... implementation specific ..." // "... implement something inherently dangerous... loading your data from a file" // "... implement something inherently dangerous... commit changes to a file ..." // "... in my implementation, I prompt user with save/quit options when edit window is closed ..." // // Apple's documentation states that NSDocumentController should *rarely* be subclassed, // but Apple fails to provide sample code to accomplish what you need. // After trying to subclass NSDocumentController, I decided to rip all that code out. // What I really needed, in my case, was this: // @interface NSObject(NSApplicationDelegate) // - (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename // // My requirements in a nutshell: // NSUndoManager, only allow 1 document open at a time (for now), multiple editing windows for 1 document, // automatically save to temporary file, ability to quit and relaunch without saving, // Custom "open", "save", "save as", "revert to saved", etc... MyDocumentController // subclass below provides a simple way to bypass much of the NSDocumentController // file dialogs, but still use other features of NSDocument (like track unsaved changes). // Override only "NSDocumentController documentForURL". /* your NSDocument subclass MUST be defined in your Info.plist. If you don't, Lion will complain with a NSLog message that looks like this: -[NSDocumentController openDocumentWithContentsOfURL:display:completionHandler:] failed during state restoration. Here's the error: Error Domain=NSCocoaErrorDomain Code=256 "The document “blah.myfiletype” could not be opened. cannot open files in the “blah Document” format." In this example, files of type .myfiletype are associated with my NSDocument subclass Personally, I hate having to put a class name in my Info.plist, because its not maintainable! Note to Apple developers: if you are going to force us to do this, then pleasssse make sure that an XCode search "In Project" for "MyDocument" finds the entry in the plist file! Oh wow, I just answered my own question: Apple! please make "All candidate files" the default option for search! MyDocument: &lt;key&gt;CFBundleDocumentTypes&lt;/key&gt; &lt;array&gt; &lt;dict&gt; &lt;key&gt;CFBundleTypeName&lt;/key&gt; &lt;string&gt;My Funky App File&lt;/string&gt; &lt;key&gt;NSDocumentClass&lt;/key&gt; &lt;string&gt;MyDocument&lt;/string&gt; &lt;key&gt;CFBundleTypeExtensions&lt;/key&gt; &lt;array&gt; &lt;string&gt;myfiletype&lt;/string&gt; &lt;/array&gt; &lt;key&gt;CFBundleTypeIconFile&lt;/key&gt; &lt;string&gt;My_File_Icon.icns&lt;/string&gt; &lt;key&gt;CFBundleTypeRole&lt;/key&gt; &lt;string&gt;Editor&lt;/string&gt; &lt;/dict&gt; &lt;/array&gt; */ #define kMyFileTypeExtension @"myfiletype" @interface MyDocument : NSDocument { } - (void) registerForUndoGrouping; - (NSString *)filePath; // convenience function - (void) setFilePath: (NSString *)filePath; // convenience function + (BOOL) openMyDocument: (NSString *)filename; // class method @end extern int gWantsToQuit; // global indicator that it's time to stop drawing/updating // track my startup state so I can control order of initialization, and so I don't // waste time drawing/updating before data is available. enum // MyLaunchStatus { kFinishedPreWaking = 0x01, // step 1: applicationWillFinishLaunching called kFinishedOpenFile = 0x02, // step 2: (optionally) application:openFile: called (important for detecting double-click file to launch app) kFinishedWaking = 0x04, // step 3: NSApp run loop ready to run. applicationDidFinishLaunching kFinishedPreLaunchCheck = 0x08, // step 4: error recovery check passed kFinishedLoadingData = 0x10, // step 5: data loaded kFinishedAndReadyToDraw = 0x20, // step 6: run loop ready for drawing }; typedef NSUInteger MyLaunchStatus; #pragma mark - @interface MyAppController : NSResponder &lt;MidiProtocol, NSOpenSavePanelDelegate, NSTextFieldDelegate, NSWindowDelegate&gt; MyDocument *toDocument; @end #pragma mark - @implementation MyDocument - (id)init { if ( !(self = [super init]) ) return self; return self; } - (void) registerForUndoGrouping { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(beginUndoGroup:) name:NSUndoManagerDidOpenUndoGroupNotification object:nil]; } - (void)canCloseDocumentWithDelegate:(id)delegate shouldCloseSelector:(SEL)shouldCloseSelector contextInfo:(void *)contextInfo { if ( [[MyAppController instance] windowShouldClose: delegate] ) if ( [delegate respondsToSelector:@selector(close)] ) [delegate close]; //[delegate performSelector:shouldCloseSelector]; //if ( [delegate isKindOfClass:[NSWindow class]] ) // [delegate performClose:self]; // :self]; return; // handled by [[MyAppController instance] windowShouldClose:(id)sender } - (void)shouldCloseWindowController:(NSWindowController *)windowController delegate:(id)delegate shouldCloseSelector:(SEL)shouldCloseSelector contextInfo:(void *)contextInfo { if ( [[MyAppController instance] windowShouldClose: [windowController window]] ) if ( [[windowController window] respondsToSelector:@selector(close)] ) [[windowController window] close]; } - (void) beginUndoGroup: (NSNotification *)iNotification { NSUndoManager *undoMgr = [self undoManager]; if ( [undoMgr groupingLevel] == 1 ) { // do your custom stuff here } } // convenience functions: - (NSString *)filePath { return [[self fileURL] path]; } - (void) setFilePath: (NSString *)filePath { if ( [filePath length] ) [self setFileURL:[NSURL fileURLWithPath: filePath]]; else [self setFileURL:nil]; } - (BOOL)validateUserInterfaceItem:(id &lt;NSValidatedUserInterfaceItem&gt;)anItem { if ( [self isDocumentEdited] &amp;&amp; [anItem action] == @selector(revertDocumentToSaved:) ) return YES; BOOL retVal = [super validateUserInterfaceItem:(id &lt;NSValidatedUserInterfaceItem&gt;)anItem]; return retVal; } - (IBAction)revertDocumentToSaved:(id)sender { NSInteger retVal = NSRunAlertPanel(@"Revert To Saved?", [NSString stringWithFormat: @"Revert to Saved File %@?", [self filePath]], @"Revert to Saved", @"Cancel", NULL); if ( retVal == NSAlertDefaultReturn ) [[MyAppController instance] myOpenFile:[self filePath]]; } + (BOOL) openMyDocument: (NSString *)filename { if ( ![[filename pathExtension] isEqualToString: kMyAppConsoleFileExtension] ) return NO; // If the user started up the application by double-clicking a file, the delegate receives the application:openFile: message FIRST MyLaunchStatus launchStatus = [[MyAppController instance] isFinishedLaunching]; BOOL userDoubleClickedToLaunchApp = !( launchStatus &amp; kFinishedPreLaunchCheck ); MyDocument *currDoc = [[MyAppController instance] document]; NSString *currPath = [currDoc filePath]; NSInteger retVal; NSLog( @"open file %@ currPath %@ launchStatus %d", filename, currPath, launchStatus ); if ( userDoubleClickedFileToLaunchApp ) { // user double-clicked a file to start MyApp currPath = [[NSUserDefaults standardUserDefaults] objectForKey:@"LastSaveFile"]; if ( [currPath isEqualToString: filename] ) { sWasAlreadyOpen = YES; if ( [[[NSUserDefaults standardUserDefaults] objectForKey:@"isDocumentEdited"] boolValue] == YES ) { retVal = NSRunAlertPanel(@"Open File", @"Revert to Saved?", @"Revert to Saved", @"Keep Changes", @"Quit", NULL); if ( retVal == NSAlertDefaultReturn ) { [[MyAppController instance] myOpenFile:filename]; } else if ( retVal == NSAlertOtherReturn ) exit(0); } // proceed with normal startup if ( currDoc ) return YES; else return NO; } } if ( !(launchStatus &amp; kFinishedPreLaunchCheck ) ) // not done launching return YES; // startup in whatever state we were before if ( [currPath isEqualToString: filename] ) { sWasAlreadyOpen = YES; NSLog( @"is edited %d currDoc %@", [currDoc isDocumentEdited], currDoc ); if ( [currDoc isDocumentEdited] ) [currDoc revertDocumentToSaved:self]; // will prompt else // document is already open, so do what Apple's standard action is... [currDoc showWindows]; } else { if ( [currDoc isDocumentEdited] ) retVal = NSRunAlertPanel(@"Open File", [NSString stringWithFormat: @"The current file has unsaved changes. Discard unsaved changes and switch to file '%@'?", filename], @"Discard unsaved changes and switch to file", @"Keep Current", NULL); else retVal = NSRunAlertPanel(@"Switch to File", [NSString stringWithFormat: @"Switch to File '%@'?\n\nCurrent file '%@'", filename, currfilePath ? currfilePath : @"Untitled"], @"Switch", @"Keep Current", NULL); if ( retVal == NSAlertDefaultReturn ) [[MyAppController instance] myOpenFile:filename]; } // user cancelled if ( currDoc ) return YES; else return NO; } // Note: readFromURL is here for completeness, but it should never be called, // because we override NSDocumentController documentForURL below. - (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError { if ( outError ) *outError = nil; if ( ![typeName isEqualToString: kMyFileTypeExtension ] ) // return NO; return YES; } // Note: writeToURL is here for completeness, but it should never be called, // because we override NSDocumentController documentForURL below. - (BOOL)writeToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError { if ( outError ) *outError = nil; return YES; } @end // kpk migrating slowly toward NSDocument framework // (currently most functionality is in MyAppController) // Must bypass default NSDocumentController behavior to allow only 1 document // and keep MyAppController responsible for read, write, dialogs, etc. @implementation MyDocumentController // this should be the only override needed to bypass NSDocument dialogs, readFromURL, // and writeToURL calls. // Note: To keep Lion happy, MainInfo.plist and Info.plist must define "MyDocument" for key "NSDocumentClass" - (id)documentForURL:(NSURL *)absoluteURL { MyDocument *currDoc = [[MyAppController instance] document]; if ( [[currDoc filePath] isEqualToString: [absoluteURL path]] ) return currDoc; else return nil; } @end #pragma mark - @implementation MyAppController static MyAppController *sInstance; + (MyAppController *)instance { return sInstance; // singleton... why is this not in all Apple's sample code? } // called by main.mm before MyAppController (or NSApp for that matter) is created. // need to init some global variables here. + (void) beforeAwakeFromNib { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; ... implementation specific ... // disable fancy stuff that slows launch down [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey: @"NSAutomaticWindowAnimationsEnabled"]; [pool release]; } - (void) awakeFromNib { NSLog(@"MyAppController awake\n"); sInstance = self; [toWindow setNextResponder:self]; [NSApp setDelegate:self]; [self addWindowToDocument:toWindow]; } - (MyDocument *)document { if ( !toDocument ) { toDocument = [[MyDocument alloc] init]; } return toDocument; } - (NSUndoManager *)undoManager { // !!! WARNING: there are multiple NSUndoManager's in this App // Note: when an editable text field is in focus, // NSTextField will create // a separate undo manager for editing text while that field is in focus. // This means that hitting undo/redo while editing a text field will not go beyond the scope of that field. // This will return the global undo manager if the keyWindow was registered // via [self addWindowToDocument:]; // Windows which are NOT part of the document (such as preferences, popups, etc.), will // return their own undoManager, and undo will do nothing while those windows are in front. // You can't undo preferences window changes, so we don't want to surprise the user. NSUndoManager *undomgr = [[NSApp keyWindow] undoManager]; if ( undomgr ) { static bool sFirstTime = true; if ( sFirstTime ) { sFirstTime = false; [undomgr setLevelsOfUndo:1000]; // set some sane limit [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(beginUndo:) name:NSUndoManagerWillUndoChangeNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(beginUndo:) name:NSUndoManagerWillRedoChangeNotification object:nil]; [toDocument registerForUndoGrouping]; // [[NSNotificationCenter defaultCenter] addObserver:self // selector:@selector(endUndo:) // name:NSUndoManagerDidUndoChangeNotification // object:nil]; // [[NSNotificationCenter defaultCenter] addObserver:self // selector:@selector(endUndo:) // name:NSUndoManagerDidRedoChangeNotification // object:nil]; } } return undomgr; } - (void) showStatusText: (id)iStatusText { ... implementation specific ... } - (void) beginUndo:(id)sender { // implementation specific stuff here NSUndoManager *undomgr = [[NSApp keyWindow] undoManager]; if ( [sender object] == undomgr ) { if ( [undomgr isUndoing] ) [self showStatusText: [NSString stringWithFormat:@"Undo %@", [undomgr undoActionName]]]; else if ( [undomgr isRedoing] ) [self showStatusText: [NSString stringWithFormat:@"Redo %@", [undomgr redoActionName]]]; } } // Add a window (with a window controller) to our document, so that the window // uses the document's NSUndoManager. In the future, we may want to use other features of NSDocument. - (void)addWindowToDocument:(NSWindow *)iWindow { NSString *autosaveName = [iWindow frameAutosaveName]; // preserve for "mainWindow", others. NSWindowController *winController = [iWindow windowController]; if ( !winController ) winController = [[NSWindowController alloc] initWithWindow:iWindow]; // create document if needed, and add window to document. [[self document] addWindowController: winController]; if ( autosaveName ) [iWindow setFrameAutosaveName:autosaveName]; // restore original for "mainWindow", others. [winController setNextResponder:self]; // keep last hotkey destination... see keyDown: } - (void) myOpenFile:(NSString*)path { // this is just a skeleton of what I do to track unsaved changes between relaunches [toDocument setFilePath:path]; ... implementation specific ... [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey:@"isDocumentEdited"]; [toDocument updateChangeCount:NSChangeCleared]; [[NSUserDefaults standardUserDefaults] setObject:[toDocument filePath] forKey:@"LastSaveFile"]; BOOL success = [[NSUserDefaults standardUserDefaults] synchronize]; // bootstrap // kpk very important... resetStandardUserDefaults forces the immutable // tree returned by dictionaryWithContentsOfFile to be mutable once re-read. // Apple: "Synchronizes any changes made to the shared user defaults object and releases it from memory. // A subsequent invocation of standardUserDefaults creates a new shared user defaults object with the standard search list." [NSUserDefaults resetStandardUserDefaults]; NSString *name = [[NSUserDefaults standardUserDefaults] objectForKey:@"LastSaveFile"]; } - (void) mySaveData:(NSString*)path { // this is just a skeleton of what I do to track unsaved changes between relaunches @try { ... implement something inherently dangerous... commit changes to a file ... if ( !errorStr ) { if ( [toDocument isDocumentEdited] ) { // UInt64 theTimeNow = VMPGlue::GetMilliS(); [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey:@"isDocumentEdited"]; [[NSUserDefaults standardUserDefaults] synchronize]; // bootstrap // DLog( @"synchronize success %d ms", (int)(VMPGlue::GetMilliS() - theTimeNow) ); } // tbd consider MyDocument saveToURL:ofType:forSaveOperation:error: [toDocument updateChangeCount:NSChangeCleared]; } @catch (...) { ... run critical alert ... } } - (void) finishLoadingData { @try { if ( dataexists ) { ... implement something inherently dangerous... loading your data from a file [toDocument setFilePath: [[NSUserDefaults standardUserDefaults] objectForKey:@"LastSaveFile"]]; NSNumber *num = [[NSUserDefaults standardUserDefaults] objectForKey:@"isDocumentEdited"]; if ( [num boolValue] == YES ) [toDocument updateChangeCount:NSChangeDone]; } else { [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:NO] forKey:@"isDocumentEdited"]; [toDocument updateChangeCount:NSChangeCleared]; } sFinishedLaunching |= kFinishedLoadingData; } @catch (...) { // !!! will not return !!! ... run critical alert ... // !!! will not return !!! } } #pragma mark NSApplication delegate // Apple: Sent directly by theApplication to the delegate. The method should open the file filename, //returning YES if the file is successfully opened, and NO otherwise. //If the user started up the application by double-clicking a file, the delegate receives the application:openFile: message before receiving applicationDidFinishLaunching:. //(applicationWillFinishLaunching: is sent before application:openFile:.) - (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename { BOOL didOpen = [MyDocument openMyDocument: filename]; sFinishedLaunching |= kFinishedOpenFile; return didOpen; } // NSApplication notification - (void) applicationDidFinishLaunching:(NSNotification*)note { // kpk note: currentEvent is often nil at this point! [[NSApp currentEvent] modifierFlags] CGEventFlags modifierFlags = CGEventSourceFlagsState(kCGEventSourceStateHIDSystemState); sFinishedLaunching |= kFinishedWaking; if ( modifierFlags &amp; (kCGEventFlagMaskShift | kCGEventFlagMaskCommand) ) { ... implementation specific ... alert: @"Shift or Command key held down at startup.\nWhat would you like to do?" title: @"Startup Options" canContinue: @"Continue" ]; } sFinishedLaunching |= kFinishedPreLaunchCheck; [self finishLoadingData]; sFinishedLaunching |= kFinishedAndReadyToDraw; } - (BOOL)windowShouldClose:(id)sender { if ( [sender isKindOfClass: [NSWindow class]] &amp;&amp; sender != toWindow ) return YES; // allow non-document-edit windows to close normally ... in my implementation, I prompt user with save/quit options when edit window is closed ... return NO; } - (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication*)sender { if ( !gWantsToQuit &amp;&amp; [toDocument isDocumentEdited] ) { if ( ![self windowShouldClose:self] ) return NSTerminateCancel; } return NSTerminateNow; } - (void) applicationWillTerminate:(NSNotification *)notification { if ( gWantsToQuit ) { ... implementation specific ... dont save potentially wonky data if relaunch is required } else { [self saveData: [toDocument filePath]]; } } @end </code></pre>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload