14

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 Recipes, which walks through creating a sample application. It's pretty good, but it's written against XCode 3.2 rather than XCode 4. So far, I've been able to bridge this myself, but I've run up against an issue where I can't figure out how to follow the instruction.

Essentially, the book goes through a sample case of subclassing NSDocumentController so that the application can handle two (eventually maybe an arbitrary number) different types of documents, and so it opens the appropriate window for each type. So, I've created a custom subclass of NSDocumentController (which the book calls VRDocumentController), and now I need to make it such that an instance of this controller loads relatively early in the application launch process. Basically, the class is a singleton, and so I have to instantiate mine before the application instantiates the standard class, and this has to be done early in the process. Fair enough.

The book cites the Apple documentation for subclassing NSDocumentController, which states that there are two ways to attack the problem: to instantiate the class in your MainMenu.xib file or to instantiate one in your -applicationWillFinishLaunching: delegate method. The Apple documentation doesm't give clear instruction on how to do either of these (more on that in a moment) and the book covers only the first version, which I think is probably my preferred method.

My problem: I cannot for the life of me pull this off in XCode 4. The instructions the book provides for XCode 3.2 are no longer accurate, because Interface Builder has been shuffled into XCode itself now and the new version of the "classes tab" no longer shows my project's classes. I found this question on Stack Overflow asking a similar question, so I tried following the accepted answer there. However, when I open the Identity Inspector and try to type VRDocumentController, it just beeps at me and doesn't take it. None of the other controller classes I've written seem to be acceptable inputs either.

I'd also be happy to go the other route; instantiating a copy in the -applicationWillFinishLaunching method. But, I have no earthly idea in which class that method actually belongs, or what its return type is. I've done a non-trivial amount of searching for that, too, with no luck.

Boris Y.
  • 4,387
  • 2
  • 32
  • 50
Luke Sneeringer
  • 9,270
  • 2
  • 35
  • 32
  • As to why you’re not being able to instantiate an object of a custom class in your nib file, I’m not sure. I’d check if VRDocumentController.{h,m} have effectively been added to the project. If you post your project online, I’d be happy to take a look at it. –  Sep 10 '11 at 18:57
  • I'd be happy to post it online. Would a (private, and I can give you access) github repo work, or is there a better method? – Luke Sneeringer Sep 10 '11 at 19:09
  • I added the classes by right clicking in the Project Navigator, clicking `Add File...`, and going through those screens. That should automatically "add it to the project", yes? If not, what do I need to do? – Luke Sneeringer Sep 10 '11 at 19:11
  • Project is at github.com/lukesneeringer/Vermont-Recipes (if anyone else wanting to help me wants to see it, leave a comment; I'd make it public, but since it's code from a book, I'm not sure that's kosher) – Luke Sneeringer Sep 10 '11 at 19:17
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/3326/discussion-between-bavarious-and-luke-sneeringer) –  Sep 10 '11 at 19:27
  • The first hit for [a search of Apple's documentation for `applicationWillFinishLaunching`](http://developer.apple.com/library/mac/search/?q=applicationWillFinishLaunching) is the NSApplicationDelegate protocol, which is where the `applicationWillFinishLaunching:` method is declared and is where you can find the method's full signature, including its return type and the type and nature of its argument. – Peter Hosey Sep 10 '11 at 21:23
  • Peter: The problem was that I am using the document application template, which didn't have a class directly extending NSApplicationDelegate. That said, I did figure it out, with Bavarious' help. – Luke Sneeringer Sep 11 '11 at 02:25
  • https://stackoverflow.com/a/35126905/2846508 is the best answer. **NSDocumentController is a singleton class; every call to its init method will return the same exact instance**, so make your NSDocumentController subclass be the first to call its init method _as soon as possible_. You can retrieve this object instance using `[[NSDocumentController alloc] init]`, `[NSDocumentController new]`, or `[NSDocumentController sharedDocumentController]`. The Apple documentation is unclear about this. But once you understand that there is only ever one NSDocumentController instance, it is simple. – Sparky May 29 '18 at 23:00

9 Answers9

15

In your application delegate:

// LukeAppDelegate.h
#import "LukeAppDelegate.h"
#import "VRDocumentController"

- (void)applicationWillFinishLaunching:(NSNotification *)notification {
    VRDocumentController *dc = [[VRDocumentController alloc] init];
}

This will make sure that an instance of VRDocumentController is created and registered as the shared document controller, preventing Cocoa from using the default NSDocumentController.

As to why you haven’t been able to use a custom object in your nib file, make sure that that you select Object (blue cube) instead of Object Controller (blue cube inside a green sphere) when dragging a new object into the nib file.


Edit: If you’re targeting an OS X version that supports restoration, -applicationWillFinishLaunching: may be too late to register a custom document controller. If the application delegate is placed inside MainMenu.xib, it should be instantiated by the nib loading process before any documents are restored, hence you can move the NSDocumentController subclass initialisation to the application delegate’s init method:

// LukeAppDelegate.h
#import "LukeAppDelegate.h"
#import "VRDocumentController"

- (id)init {
    self = [super init];
    VRDocumentController *dc = [[VRDocumentController alloc] init];
    return self;
}
  • I don't see anything that looks anything like `AppDelegate.h` or `AppDelegate.m`, with or without any prefix. Can I make one after the project has started? – Luke Sneeringer Sep 10 '11 at 19:09
  • You can but the standard Cocoa application template should’ve created one for you. Also, the application delegate is usually an object of a custom class in MainMenu.nib, much like what you want with VRDocumentController. –  Sep 10 '11 at 19:13
  • Hmm. I used the document-based application template, which created a MyDocument class instead. Does that perhaps function as the delegate? – Luke Sneeringer Sep 10 '11 at 19:17
  • OK, Bavarious found the past piece of the issue. The answer is to go to "Object Library", scroll down to "Object" (it has an image of a blue cube), drag it to the objects list in the XIB, and then edit the class name. – Luke Sneeringer Sep 10 '11 at 19:37
  • 1
    IMHO this approach will not work if like to handle the NSWindowRestoration sequence, this is already done at the time when applicationWillFinishLaunching is called. – Stephan Dec 09 '12 at 13:55
  • Why was this answer marked as correct? @Stephan is correct. If your app has window restore on (which it does by default in document-based apps) then this answer will fail to associate your custom NSDocumentController subclass with the shared instance. – zakdances Aug 30 '13 at 10:29
  • Even if you create an app delegate using the approach described by @LukeSneeringer , this answer still fails in document-based apps because the restored document loads an `NSDocumentController` instance **before** the app delegate calls `applicationWillFinishLaunching`. – zakdances Aug 30 '13 at 10:34
  • @yourfriendzak This answer predates the introduction of restoration into Cocoa and OS X. I’ve added a note about the restoration issue. –  Sep 03 '13 at 08:06
  • How shall I remove the subsequent warning (unused object)? Is `__unused` fine? – Colas Mar 24 '14 at 18:53
7

The answer marked correct (@Bavarious'answer) fails to work on document-based apps because documents often load before any applicationWillFinishLaunching: is called. After reading the helpful clues in @Graham Perks answer, I tried a few different approaches and this seems to work reliably:

@implementation AppDelegate

- (id)init
{
    self = [super init];
    if (self) {
        MyDocumentController *dc = [[MyDocumentController alloc] init];
        if (dc) {};
    }
    return self;
}

Note Obviously (if not already created) you'll need to create an AppDelegate yourself first and link it in your MainMenu.XIB.

zakdances
  • 22,285
  • 32
  • 102
  • 173
  • In your tests, does it fail if you instantiate the application delegate inside MainMenu.xib? I.e., does restoration happen before MainMenu.xib is loaded? –  Sep 03 '13 at 08:08
  • Restoration doesn't happen before `- (id)init` is called regardless of what it's linked to in IB. – zakdances Sep 03 '13 at 17:52
  • Is there any assertion about main thread and document controller methods invocations? I ask cause I want to put document controller into service instance ( which also should be shared ) to prevent huge appDelegate methods size. – gaussblurinc Apr 06 '18 at 10:31
7

Easier method of getting the Custom Document Controller created in time is to implement the + (void) load method:

This is literally all you need to do it implement a custom document controller.

@implementation AppDocumentController

+ (void) load
{
    [AppDocumentController new];
}

// ... your overriding goes here

@end

This is cool because there's no separation of the codez. Not 100% sure if the controller is created too early, but works well for me.

Leslie Godwin
  • 2,601
  • 26
  • 18
  • This is the best answer! There is only ever one NSDocumentController instance in an application. Sending init multiple times to NSDocumentController will only ever return the same object instance, unlike most other class initializer methods which initialize new instances. Therefore, when a subclass of NSDocumentController sets itself by the NSDocumentController's init, NSDocumentController makes an instance of that subclass the only possible NSDocumentController in the application. Calling init again will always return the same as calling sharedDocumentController. So the sooner, the better! – Sparky May 29 '18 at 22:51
  • Excellent answer. [Also shown](https://how2.galaxyverge.com/NSDocumentController.txt) with a complete example at Galaxy Verge. – Sparky Apr 14 '19 at 10:59
5

You can subclass easily in Swift 3:

class AppDelegate: NSObject {
    let docController = DocController()
}

class DocController: NSDocumentController {
}
feca
  • 1,119
  • 16
  • 14
4

Just create an object in the main storyboard, set it's class as your NSDocumentController subclass, and it will be used as the application shared document controller.

enter image description here

Moose
  • 2,607
  • 24
  • 23
2

Here's a solution:

// In MyDocumentController.h
@interface MyDocumentController : NSDocumentController
@end

// In MyDocumentController.m
@implementation MyDocumentController
  // ... your custom code here
@end

// In MyAppDelegate.h
@interface AppDelegate : NSObject <NSApplicationDelegate>
@property (nonatomic, strong) IBOutlet MyDocumentController *myController;
@end

Now, go into MainMenu.xib and add a custom object to your nib. Be sure to use the inspector on this object, and in the third pane of the inspector, set the custom class to MyDocumentController.

Now wire this object into your outlet by ctrl-clicking on your new object in the left list of things in the nib and drag (while still ctrl-clicking) to App Delegate. Release and it should flash and show myController. Click that and you're all set.

Now you can test that you're getting your custom controller with the following code:

// In MyAppDelegate.m
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    NSLog(@"sharedController is %@", [NSDocumentController sharedController]);
}

This should print something like

<MyDocumentController: 0x600000044710>

You're done!

The key is to wire in your custom MyDocumentController in MainMenu.xib. If you try to just initialize it in applicationDidFinishLaunching, it's often too late and AppKit has already created and set sharedDocumentController (of which there can only be one).

Also, the outlet needs to be strong not weak because your custom controller is a top-level object in the nib not referenced by anything else in the nib.

This works for me on multiple versions of OS X and Xcode.

Please mark this as the correct answer if it works for you! :-)

Joel
  • 2,285
  • 2
  • 21
  • 22
1

If - as can sometimes happen - your NSDocumentController subclass seems to randomly NOT be returned as the sharedDocumentController, add a breakpoint on -[NSDocumentController init] and see on the stack what bit of your code is (probably accidentally) creating the regular NSDocumentController. The first controller created is always returned from sharedDocumentController.

This can happen when something in your XIB file gets init'd and (even indirectly) invokes sharedDocumentController. The order of object creation during XIB file loading isn't deterministic, so sometimes your NSDocumentController subclass will be created first, other times another offending object gets init'd first and subsequently your subclass gets ignored.

You'll have to move the other object's creation to a later point (e.g. applicationDidFinishLaunching:). Or have the object created at the same time, but delay whatever initialization its doing.

(Just ran into this with an inspector panel screwing things up, thought it worth noting here!)

Graham Perks
  • 23,007
  • 8
  • 61
  • 83
  • When a document-based app is opened, the document that was open from the last session immediately loads before the AppDelegate. How can this be stopped? – zakdances Aug 30 '13 at 09:56
0

Had to spent a couple of hours figuring it out in nowadays Xcode (14.1) interface builder and here are my findings:

  1. In the Object Library of interface builder find Object: enter image description here

  2. Drag-n-Drop it under your Application Scene:

enter image description here

  1. Specify your custom class under Class field of the newly created object's Identity Inspector:

enter image description here

And you are done

The Dreams Wind
  • 8,416
  • 2
  • 19
  • 49
-2
// 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:
<key>CFBundleDocumentTypes</key>
<array>
    <dict>
        <key>CFBundleTypeName</key>
        <string>My Funky App File</string>
        <key>NSDocumentClass</key>
        <string>MyDocument</string>
        <key>CFBundleTypeExtensions</key>
        <array>
            <string>myfiletype</string>
        </array>
        <key>CFBundleTypeIconFile</key>
        <string>My_File_Icon.icns</string>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
    </dict>
</array>
*/
#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 <MidiProtocol, NSOpenSavePanelDelegate, NSTextFieldDelegate, NSWindowDelegate>
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 <NSValidatedUserInterfaceItem>)anItem
{
   if ( [self isDocumentEdited] && [anItem action] == @selector(revertDocumentToSaved:) )
      return YES;
   BOOL retVal = [super validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)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
Keith Knauber
  • 752
  • 6
  • 13