应用程序终止期间NSDocument saveDocumentWithDelegate死锁

时间:2013-05-03 14:08:57

标签: objective-c modal-dialog synchronous nsdocument

NSDocument仍然是软件维护的噩梦。

其他人有问题,他们希望同步处理某些阻止对话框?

开始编辑:我可能找到了一个允许我同步等待的解决方案

任何人都可以验证这是否是“Apple批准”的解决方案?

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

我必须支持Snow Leopard / Lion / ML

应用终止是一个丑陋的过程。 当用户决定退出,并且文档有需要保存的更改时,我称之为:

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

真的非常希望这个调用是同步的,但在最新的Lion中,这会挂起我的应用程序:

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

我最好的猜测是关闭了一个窗口关闭按钮的mouseDown: 令人困惑的是NSDocument。

所以现在,我必须返回,并使用不可维护的状态机逻辑来管理我的应用程序主循环,以防止用户执行各种危险的热键。

好的,所以我咧嘴一笑,忍受了另一个障碍!

在以前的OS版本/ SDK中,[NSApp modalWindow]会返回一个窗口 是在这种状态。现在它没有! Grrrrr ...
NSDocument没有API来测试它何时处于这种状态!

所以,现在没有机制来全局检查这种状态! 我必须在状态机中添加另一个状态变量。

任何人都有一个更清晰的解决方案可以解决这个问题,该解决方案适用于所有操作系统版本以及所有现有(以及将来)的SDK?

2 个答案:

答案 0 :(得分:2)

更好的方法是将未保存的文档保存在链中。这很容易:

// 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 :(得分:1)

也许这个答案为时已晚,无法使用但是...在我实现的一个应用程序中 - (IBAction)终止:我的NSApplication派生类中的(id)发送者有条件地调用[super terminate]来实际关闭仅当所有打开的文档都已完全保存时才应用。我可能在Apple文档或其他示例中找到了一些。

终止覆盖将遍历每个文档并关闭它(因为它已保存),或者在NSDocument派生类中调用文档的canCloseDocumentWithDelegate方法传递“自我”。并且&#39;终止&#39;作为didSaveSelector。由于terminate方法失败并且除了使文档呈现NSAlert之外什么都不做,因此如果用户单击YES或NO,文档类中的警报将回调并重新运行终止例程。如果所有文档都是干净的,应用程序将终止,因为[super terminate]将被调用。如果存在任何更脏的文档,则重复该过程。

例如:

@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

后来在文档派生类中:

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

应该这样做 - 没有循环......没有等待......