2
votes

NSDocument continues to be a software maintenance nightmare.

Anyone else having a problem where they want certain blocking dialogs to be handled SYNCHRONOUSLY?

BEGIN EDIT: I may have found a solution that allows me to wait synchronously

Can anyone verify that this would be an "Apple approved" solution?

static BOOL sWaitingForDidSaveModally = NO;
BOOL gWaitingForDidSaveCallback = NO; // NSDocument dialog calls didSave: when done



...
  gWaitingForDidSaveCallback = true;
  [toDocument saveDocumentWithDelegate:self 
                       didSaveSelector:@selector(document:didSave:contextInfo:)  
                           contextInfo:nil];
   if ( gWaitingForDidSaveCallback )
   {
      // first, dispatch any other potential alerts synchronously
      while ( gWaitingForDidSaveCallback && [NSApp modalWindow] )
         [NSApp runModalForWindow: [NSApp modalWindow]];

      if ( gWaitingForDidSaveCallback )
      {
         sWaitingForDidSaveModally = YES;
         [NSApp runModalForWindow: [NSApp mbWindow]]; // mbWindow is our big (singleton) window
         sWaitingForDidSaveModally = NO;
      }
   }
...


- (void)document:(NSDocument *)doc didSave:(BOOL)didSave contextInfo:(void  *)contextInfo
{
   [self recordLastSaveURL];
   gWaitingForDidSaveCallback = NO;
   if ( sWaitingForDidSaveModally )
      [NSApp stopModal];

}

END EDIT

I have to support Snow Leopard/Lion/ML

App termination is an ugly process. When the user decides to quit, and the document has changes that need saving, I call this:

  gWaitingForDidSaveCallback = true;
  [toDocument saveDocumentWithDelegate:self 
                       didSaveSelector:@selector(document:didSave:contextInfo:)  
                           contextInfo:nil];

I really really really want this call to be synchronous, but in latest Lion, this hangs my app:

   while ( gWaitingForDidSaveCallback )
   {
      // didSave: callback clears sWaitingForDidSaveCallback
      // do my own synchronous wait for now
      [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceReferenceDate:0.05]];
   }

My best guess for the hang is that the mouseDown: of a window close button is confusing the NSDocument.

So now, I have to return, and pepper my apps main loop with unmaintainable state machine logic to prevent user from executing various dangerous hotkeys.

Ok, so I grin and bear it, and run into yet another roadblock!

In previous OS versions/SDKs, [NSApp modalWindow] would return a window when it was in this state. Now it doesn't! Grrrrr...
NSDocument has no API to test when it is in this state!

So, now there is no mechanism to globally check this state! I have to add yet another state variable to my state machine.

Anyone have a cleaner solution for this problem that works in all OS versions and all present (and future) SDKs?

2

2 Answers

2
votes

The better way is to save unsaved documents in chain. It is very easy:

// Catch application terminate event
-(NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
  NSDocumentController *dc = [NSDocumentController sharedDocumentController];
  for (NSInteger i = 0; i < [[dc documents] count]; i++)
  {
    Document *doc = [[dc documents] objectAtIndex:i];
    if ([doc isDocumentEdited])
    {
      // Save first unsaved document
      [doc saveDocumentWithDelegate:self
                    didSaveSelector:@selector(document:didSave:contextInfo:)
                        contextInfo:(__bridge void *)([NSNumber numberWithInteger:i + 1])]; // Next document
      return NSTerminateLater;  // Wait until last document in chain will be saved
    }
  }
  return NSTerminateNow;  // All documents are saved or there are no open documents. Terminate.
}

...

// Document saving finished
-(void)document:(NSDocument *)doc didSave:(BOOL)didSave contextInfo:(void  *)contextInfo
{
  if (didSave)  // Save button pressed
  {
    NSDocumentController *dc = [NSDocumentController sharedDocumentController];
    NSInteger nextIndex = [(__bridge NSNumber *)contextInfo integerValue];
    for (NSInteger i = nextIndex; i < [[dc documents] count]; i++)
    {
      Document *doc = [[dc documents] objectAtIndex:nextIndex];
      if ([doc isDocumentEdited])
      {
        // Save next unsaved document
        [doc saveDocumentWithDelegate:self
                      didSaveSelector:@selector(document:didSave:contextInfo:)
                          contextInfo:(__bridge void *)([NSNumber numberWithInteger:nextIndex + 1])]; // Next document
        return;
      }
    }
    [NSApp replyToApplicationShouldTerminate:YES];    // All documents saved. Terminate.
  }
  else [NSApp replyToApplicationShouldTerminate:NO];  // Saving canceled. Terminate canceled.

}

1
votes

Maybe this answer is too late to be useful but... In one of my apps I implemented -(IBAction)terminate:(id)sender in my NSApplication derived class which would conditionally call [super terminate] to actually close the application only if all open documents were cleanly saved. I may have found some of this in the Apple docs or other examples.

The terminate override will go through each document and either close it (because it's saved), or call the document's canCloseDocumentWithDelegate method in the NSDocument derived class passing 'self' and 'terminate' as the didSaveSelector. Since the terminate method falls through and does nothing except make the document present an NSAlert, the alert in the document class will callback and re-run the terminate routine if the user clicks YES or NO. If all documents are clean, the app will terminate since [super terminate] will get called. If any more dirty documents exist, the process repeats.

For example:

@interface MyApplication : NSApplication 
@end

@implementation MyApplication

- (IBAction)terminate:(id)sender
{
    //Loop through and find any unsaved document to warn the user about.
    //Close any saved documents along the way.
    NSDocument *docWarn = NULL;
    NSArray *documents = [[NSDocumentController sharedDocumentController] documents];
    for(int i = 0; i < [documents count]; i++)
    {
        NSDocument *doc = [documents objectAtIndex:i];
        if([doc isDocumentEdited])
        {
            if(docWarn == NULL || [[doc windowForSheet] isKeyWindow])
                docWarn = doc;
        }
        else
        {
            //close any document that doesn't need saving.  this will 
            //also close anything that was dirty that the user answered
            //NO to on the previous call to this routine which triggered
            //a save prompt.
            [doc close];
        }
    }
    if(docWarn != NULL)
    {
        [[docWarn windowForSheet] orderFront:self];
        [[docWarn windowForSheet] becomeFirstResponder];
        [docWarn canCloseDocumentWithDelegate:self shouldCloseSelector:@selector(terminate:) contextInfo:NULL];
    }
    else 
    {
        [super terminate:sender];
    }
}

@end

Later in the document derived class:

typedef struct {

    void * delegate;
    SEL shouldCloseSelector;
    void *contextInfo;

} CanCloseAlertContext; 

@interface MyDocument : NSDocument
@end

@implementation MyDocument

- (void)canCloseDocumentWithDelegate:(id)inDelegate shouldCloseSelector:(SEL)inShouldCloseSelector contextInfo:(void *)inContextInfo 
{
    // This method may or may not have to actually present the alert sheet.
    if (![self isDocumentEdited]) 
    {
        // There's nothing to do.  Tell the delegate to continue with the close. 
        if (inShouldCloseSelector) 
        {
            void (*callback)(id, SEL, NSDocument *, BOOL, void *) = (void (*)(id, SEL, NSDocument *, BOOL, void *))objc_msgSend;
            (callback)(inDelegate, inShouldCloseSelector, self, YES, inContextInfo);
        }
    } 
    else 
    {
        NSWindow *documentWindow = [self windowForSheet];

        // Create a record of the context in which the panel is being 
        // shown, so we can finish up when it's dismissed.

        CanCloseAlertContext *closeAlertContext = malloc(sizeof(CanCloseAlertContext));

        closeAlertContext->delegate = (__bridge void *)inDelegate;
        closeAlertContext->shouldCloseSelector = inShouldCloseSelector;
        closeAlertContext->contextInfo = inContextInfo;

        // Present a "save changes?" alert as a document-modal sheet.
        [documentWindow makeKeyAndOrderFront:nil];

        NSBeginAlertSheet(@"Would you like to save your changes?", @"Yes", @"Cancel", @"No", documentWindow, self, 
                          @selector(canCloseAlertSheet:didEndAndReturn:withContextInfo:), NULL, closeAlertContext, @"%");

    }

} 

- (void)canCloseAlertSheet:(NSWindow *)inAlertSheet didEndAndReturn:(int)inReturnCode withContextInfo:(void *)inContextInfo 
{
    CanCloseAlertContext *canCloseAlertContext = inContextInfo;
    void (*callback)(id, SEL, NSDocument *, BOOL, void* ) = (void (*)(id, SEL, NSDocument *, BOOL, void* ))objc_msgSend;

    if (inAlertSheet) [inAlertSheet orderOut:self];

    // The user's dismissed our "save changes?" alert sheet. What happens next depends on how the dismissal was done.
    if (inReturnCode==NSAlertAlternateReturn) 
    {
        //Cancel - do nothing.
    }
    else if (inReturnCode==NSAlertDefaultReturn) 
    {
        //Yes - save the current document

        [self saveDocumentWithDelegate:(__bridge id)canCloseAlertContext->delegate 
                       didSaveSelector:canCloseAlertContext->shouldCloseSelector contextInfo:canCloseAlertContext->contextInfo];
    } 
    else 
    {

        // No - just clear the dirty flag and post a message to
        //      re-call the shouldCloseSelector.  This should be 
        //      the app:terminate routine.
        [self clearDirtyFlag];

        if (canCloseAlertContext->shouldCloseSelector) 
        {
            (callback)((__bridge id)canCloseAlertContext->delegate, 
                       canCloseAlertContext->shouldCloseSelector, self, YES, canCloseAlertContext->contextInfo);

        }

    }

    // Free up the memory that was allocated in -canCloseDocumentWithDelegate:shouldCloseSelector:contextInfo:.

    free(canCloseAlertContext);

}
@end

And that should do it - No loops... no waiting...