Search code examples
objective-ccocoansdocumentcontroller

How to subclass NSDocumentController to only allow one doc at a time


I'm trying to create a Core Data, document based app but with the limitation that only one document can be viewed at a time (it's an audio app and wouldn't make sense for a lot of docs to be making noise at once).

My plan was to subclass NSDocumentController in a way that doesn't require linking it up to any of the menu's actions. This has been going reasonably but I've run into a problem that's making me question my approach a little.

The below code works for the most part except if a user does the following: - Tries to open a doc with an existing 'dirty' doc open - Clicks cancel on the save/dont save/cancel alert (this works ok) - Then tries to open a doc again. For some reason now the openDocumentWithContentsOfURL method never gets called again, even though the open dialog appears.

Can anyone help me work out why? Or perhaps point me to an example of how to do this right? It feels like something that must have been implemented by a few people but I've not been able to find a 10.7+ example.

- (BOOL)presentError:(NSError *)error
{
    if([error.domain isEqualToString:DOCS_ERROR_DOMAIN] && error.code == MULTIPLE_DOCS_ERROR_CODE)
        return NO;
    else
        return [super presentError:error];
}

- (id)openUntitledDocumentAndDisplay:(BOOL)displayDocument error:(NSError **)outError
{
    if(self.currentDocument) {
        [self closeAllDocumentsWithDelegate:self
                        didCloseAllSelector:@selector(openUntitledDocumentAndDisplayIfClosedAll: didCloseAll: contextInfo:)
                                contextInfo:nil];

        NSMutableDictionary* details = [NSMutableDictionary dictionary];
        [details setValue:@"Suppressed multiple documents" forKey:NSLocalizedDescriptionKey];
        *outError = [NSError errorWithDomain:DOCS_ERROR_DOMAIN code:MULTIPLE_DOCS_ERROR_CODE userInfo:details];
        return nil;
    }

    return  [super openUntitledDocumentAndDisplay:displayDocument error:outError];
}

- (void)openUntitledDocumentAndDisplayIfClosedAll:(NSDocumentController *)docController
                                      didCloseAll: (BOOL)didCloseAll
                                      contextInfo:(void *)contextInfo
{
    if(self.currentDocument == nil)
        [super openUntitledDocumentAndDisplay:YES error:nil];
}

- (void)openDocumentWithContentsOfURL:(NSURL *)url
                              display:(BOOL)displayDocument
                    completionHandler:(void (^)(NSDocument *document, BOOL documentWasAlreadyOpen, NSError *error))completionHandler NS_AVAILABLE_MAC(10_7)
{
    NSLog(@"%s", __func__);
    if(self.currentDocument) {
        NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:[url copy], @"url",
                                                                        [completionHandler copy], @"completionHandler",
                                                                        nil];
        [self closeAllDocumentsWithDelegate:self
                        didCloseAllSelector:@selector(openDocumentWithContentsOfURLIfClosedAll:didCloseAll:contextInfo:)
                                contextInfo:(__bridge_retained void *)(info)];
    } else {
        [super openDocumentWithContentsOfURL:url display:displayDocument completionHandler:completionHandler];
    }
}

- (void)openDocumentWithContentsOfURLIfClosedAll:(NSDocumentController *)docController
                                     didCloseAll: (BOOL)didCloseAll
                                     contextInfo:(void *)contextInfo
{
    NSDictionary *info = (__bridge NSDictionary *)contextInfo;
    if(self.currentDocument == nil)
        [super openDocumentWithContentsOfURL:[info objectForKey:@"url"] display:YES completionHandler:[info objectForKey:@"completionHandler"]];
}

Solution

  • There's a very informative exchange on Apple's cocoa-dev mailing list that describes what you have to do in order to subclass NSDocumentController for your purposes. The result is that an existing document is closed when a new one is opened.

    Something else you might consider is to mute or stop playing a document when its window resigns main (i.e., sends NSWindowDidResignMainNotification to the window's delegate), if only to avoid forcing what might seem to be an artificial restriction on the user.