How do I use a subclass of NSDocumentController in XCode 4?

前端 未结 7 1433
陌清茗
陌清茗 2020-12-31 11:34

I am currently in the process of trying to teach myself Cocoa development. Toward this end, I purchased a mostly-excellent book, Cocoa Recipes for Mac OS X: Vermont Reci

7条回答
  •  春和景丽
    2020-12-31 11:58

    // 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:
    CFBundleDocumentTypes
    
        
            CFBundleTypeName
            My Funky App File
            NSDocumentClass
            MyDocument
            CFBundleTypeExtensions
            
                myfiletype
            
            CFBundleTypeIconFile
            My_File_Icon.icns
            CFBundleTypeRole
            Editor
        
    
    */
    #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 
    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 )anItem
    {
       if ( [self isDocumentEdited] && [anItem action] == @selector(revertDocumentToSaved:) )
          return YES;
       BOOL retVal = [super validateUserInterfaceItem:(id )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 & 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 & 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 & (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]] && 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 && [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
    

提交回复
热议问题