Question

I have an NSOpenPanel and I want to do some validation of the selection after the user has clicked OK. My code is simple:

void (^openPanelHandler)(NSInteger) = ^(NSInteger returnCode) {
    if (returnCode == NSFileHandlingPanelOKButton) {
        // do my validation
        [self presentError:error]; // uh oh, something bad happened
    }
}

[openPanel beginSheetModalForWindow:[self window]
                  completionHandler:openPanelHandler];

[self window] is an application-modal window. The panel opens as a sheet. So far so good.

Apple's docs say that the completion handler is supposed to be called "after the user has closed the panel." But in my case, it's called immediately upon the "OK/Cancel" button press, not upon the panel having closed. The effect of this is that the error alert opens above the open panel, not after the panel has closed. It still works, but it's not Mac-like.

What I would prefer is for the user to click OK, the open panel sheet to fold up, then the alert sheet to appear.

I guess I could present the alert using a delayed selector, but that seems like a hack.

Was it helpful?

Solution

Since the panel completion handler is invoked before the panel has effectively been closed,1 one solution is to observe NSWindowDidEndSheetNotification on your modal window:

  1. Declare an instance variable/property in your class to hold the validation error;
  2. Declare a method that will be executed when the panel is effectively closed. Define it so that if presents the error on the current window;
  3. Have your class listen to NSWindowDidEndSheetNotification on [self window], executing the method declared above when the notification is sent;
  4. In the panel completion handler, if the validation fails then assign the error to the instance variable/property declared above.

By doing this, the completion handler will only set the validation error. Soon after the handler is invoked, the open panel is closed and the notification will be sent to your object, which in turn presents the validation error that has been set by the completion handler.

For example:

In your class declaration, add:

@property (retain) NSError *validationError;
- (void)openPanelDidClose:(NSNotification *)notification;

In your class implementation, add:

@synthesize validationError;

- (void)dealloc {
    [validationError release];
    [super dealloc];
}

- (void)openPanelDidClose:(NSNotification *)notification {
    if (self.validationError) [self presentError:error];
    // or [[self window] presentError:error];

    // Clear validationError so that further notifications
    // don't show the error unless a new error has been set
    self.validationError = nil;

    // If [self window] presents other sheets, you don't
    // want this method to be fired for them
    [[NSNotificationCenter defaultCenter] removeObserver:self
        name:NSWindowDidEndSheetNotification
        object:[self window]];
}

// Assuming an action fires the open panel
- (IBAction)showOpenPanel:(id)sender {
    NSOpenPanel *openPanel = [NSOpenPanel openPanel];

    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(openPanelDidClose:)
        name:NSWindowDidEndSheetNotification
        object:[self window]];

    void (^openPanelHandler)(NSInteger) = ^(NSInteger returnCode) {
        if (returnCode == NSFileHandlingPanelOKButton) {
            // do my validation
            // uh oh, something bad happened
            self.validationError = error;
        }
    };

    [openPanel beginSheetModalForWindow:[self window]
                      completionHandler:openPanelHandler];

}

1If you think this behaviour is wrong, consider filing a bug report with Apple. I don’t really remember whether an error should be presented over an open/save panel.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top