Wait for [NSAlert beginSheetModalForWindow:...];
Asked Answered
Z

12

21

When I display an NSAlert like this, I get the response straight away:

int response;
NSAlert *alert = [NSAlert alertWithMessageText:... ...];
response = [alert runModal];

The problem is that this is application-modal and my application is document based. I display the alert in the current document's window by using sheets, like this:

int response;
NSAlert *alert = [NSAlert alertWithMessageText:... ...];
[alert beginSheetModalForWindow:aWindow
                  modalDelegate:self
                 didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
                    contextInfo:&response];

//elsewhere
- (void) alertDidEnd:(NSAlert *) alert returnCode:(int) returnCode contextInfo:(int *) contextInfo
{
    *contextInfo = returnCode;
}

The only issue with this is that beginSheetModalForWindow: returns straight away so I cannot reliably ask the user a question and wait for a response. This wouldn't be a big deal if I could split the task into two areas but I can't.

I have a loop that processes about 40 different objects (that are in a tree). If one object fails, I want the alert to show and ask the user whether to continue or abort (continue processing at the current branch), but since my application is document based, the Apple Human Interface Guidelines dictate to use sheets when the alert is specific to a document.

How can I display the alert sheet and wait for a response?

Zea answered 3/3, 2009 at 1:22 Comment(0)
O
3

Unfortunately, there is not much you can do here. You basically have to make a decision: re-architect your application so that it can process the object in an asynchronous manner or use the non-approved, deprecated architecture of presenting application modal alerts.

Without knowing any information about your actual design and how you processes these objects, it's hard to give any further information. Off the top of my head, though, a couple of thoughts might be:

  • Process the objects in another thread that communicates with the main thread through some kind of run loop signal or queue. If the window's object tree gets interrupted, it signals the main thread that it was interrupted and waits on a signal from the main thread with information about what to do (continue this branch or abort). The main thread then presents the document-modal window and signals the process thread after the user chooses what to do.

This may be really over-complicated for what you need, however. In that case, my recommendation would be to just go with the deprecated usage, but it really depends on your user requirements.

Orabelle answered 3/3, 2009 at 2:29 Comment(2)
Threads my ultimately be the way to go I suppose. The object tree is eventually going to get bigger and more complicated.Zea
Without seeing your app, it's obviously hard to say, but are you really sure you need threads? I have never encountered the case where putting the response in the callback method was more complex than threading the app.Lefthanded
G
14

We created a category on NSAlert to run alerts synchronously, just like application-modal dialogs:

NSInteger result;

// Run the alert as a sheet on the main window
result = [alert runModalSheet];

// Run the alert as a sheet on some other window
result = [alert runModalSheetForWindow:window];

The code is available via GitHub, and the current version posted below for completeness.


Header file NSAlert+SynchronousSheet.h:

#import <Cocoa/Cocoa.h>


@interface NSAlert (SynchronousSheet)

-(NSInteger) runModalSheetForWindow:(NSWindow *)aWindow;
-(NSInteger) runModalSheet;

@end

Implementation file NSAlert+SynchronousSheet.m:

#import "NSAlert+SynchronousSheet.h"


// Private methods -- use prefixes to avoid collisions with Apple's methods
@interface NSAlert ()
-(IBAction) BE_stopSynchronousSheet:(id)sender;   // hide sheet & stop modal
-(void) BE_beginSheetModalForWindow:(NSWindow *)aWindow;
@end


@implementation NSAlert (SynchronousSheet)

-(NSInteger) runModalSheetForWindow:(NSWindow *)aWindow {
    // Set ourselves as the target for button clicks
    for (NSButton *button in [self buttons]) {
        [button setTarget:self];
        [button setAction:@selector(BE_stopSynchronousSheet:)];
    }

    // Bring up the sheet and wait until stopSynchronousSheet is triggered by a button click
    [self performSelectorOnMainThread:@selector(BE_beginSheetModalForWindow:) withObject:aWindow waitUntilDone:YES];
    NSInteger modalCode = [NSApp runModalForWindow:[self window]];

    // This is called only after stopSynchronousSheet is called (that is,
    // one of the buttons is clicked)
    [NSApp performSelectorOnMainThread:@selector(endSheet:) withObject:[self window] waitUntilDone:YES];

    // Remove the sheet from the screen
    [[self window] performSelectorOnMainThread:@selector(orderOut:) withObject:self waitUntilDone:YES];

    return modalCode;
}

-(NSInteger) runModalSheet {
    return [self runModalSheetForWindow:[NSApp mainWindow]];
}


#pragma mark Private methods

-(IBAction) BE_stopSynchronousSheet:(id)sender {
    // See which of the buttons was clicked
    NSUInteger clickedButtonIndex = [[self buttons] indexOfObject:sender];

    // Be consistent with Apple's documentation (see NSAlert's addButtonWithTitle) so that
    // the fourth button is numbered NSAlertThirdButtonReturn + 1, and so on
    NSInteger modalCode = 0;
    if (clickedButtonIndex == NSAlertFirstButtonReturn)
        modalCode = NSAlertFirstButtonReturn;
    else if (clickedButtonIndex == NSAlertSecondButtonReturn)
        modalCode = NSAlertSecondButtonReturn;
    else if (clickedButtonIndex == NSAlertThirdButtonReturn)
        modalCode = NSAlertThirdButtonReturn;
    else
        modalCode = NSAlertThirdButtonReturn + (clickedButtonIndex - 2);

    [NSApp stopModalWithCode:modalCode];
}

-(void) BE_beginSheetModalForWindow:(NSWindow *)aWindow {
    [self beginSheetModalForWindow:aWindow modalDelegate:nil didEndSelector:nil contextInfo:nil];
}

@end
Greenling answered 10/6, 2011 at 16:31 Comment(0)
S
10

Here is a NSAlert category that solves the issue (as suggested by Philipp with the solution proposed by Frederick and improved by Laurent P.: I use a code block instead of a delegate, so it is simplified once again).

@implementation NSAlert (Cat)

-(NSInteger) runModalSheetForWindow:(NSWindow *)aWindow
{
    [self beginSheetModalForWindow:aWindow completionHandler:^(NSModalResponse returnCode)
        { [NSApp stopModalWithCode:returnCode]; } ];
    NSInteger modalCode = [NSApp runModalForWindow:[self window]];
    return modalCode;
}

-(NSInteger) runModalSheet {
    return [self runModalSheetForWindow:[NSApp mainWindow]];
}

@end
Stull answered 21/5, 2014 at 10:54 Comment(2)
Perfect! Just the essence of what is required to make it work. Thanks.Kile
@Stull - This is a great solution, but for one issue: is there a way for the sheet to be halting events only in the window containing the sheet? It seems as if NSApp runModalForWindow halts ALL event processing in the app's other windows, too.Thomasina
S
9

The solution is to call

[NSApp runModalForWindow:alert];

after beginSheetModalForWindow. Also, you need to implement a delegate that catches the "dialog has closed" action, and calls [NSApp stopModal] in response.

Someone answered 6/4, 2010 at 18:37 Comment(2)
I use [NSApp stopModalWithCode:returnCode]; so that runModalForWindow gets the correct code.Camise
As of 10.9, -beginSheetModalForWindow:completionHandler: is preferred over using a delegate. Laurent's stopModalWithCode:returnCode in the completion block works: The user clicks, the completion block runs, then the returnCode is returned by runModalForWindow.Wendt
L
5

Just in case anyone comes looking for this (I did), I solved this with the following:

@interface AlertSync: NSObject {
    NSInteger returnCode;
}

- (id) initWithAlert: (NSAlert*) alert asSheetForWindow: (NSWindow*) window;
- (NSInteger) run;

@end

@implementation AlertSync
- (id) initWithAlert: (NSAlert*) alert asSheetForWindow: (NSWindow*) window {
    self = [super init];

    [alert beginSheetModalForWindow: window
           modalDelegate: self didEndSelector: @selector(alertDidEnd:returnCode:) contextInfo: NULL];

    return self;
}

- (NSInteger) run {
    [[NSApplication sharedApplication] run];
    return returnCode;
}

- (void) alertDidEnd: (NSAlert*) alert returnCode: (NSInteger) aReturnCode {
    returnCode = aReturnCode;
    [[NSApplication sharedApplication] stopModal];
}
@end

Then running an NSAlert synchronously is as simple as:

AlertSync* sync = [[AlertSync alloc] initWithAlert: alert asSheetForWindow: window];
int returnCode = [sync run];
[sync release];

Note there is potential for re-entrancy issues as discussed, so be careful if doing this.

Lustrous answered 25/1, 2010 at 1:33 Comment(0)
O
3

Unfortunately, there is not much you can do here. You basically have to make a decision: re-architect your application so that it can process the object in an asynchronous manner or use the non-approved, deprecated architecture of presenting application modal alerts.

Without knowing any information about your actual design and how you processes these objects, it's hard to give any further information. Off the top of my head, though, a couple of thoughts might be:

  • Process the objects in another thread that communicates with the main thread through some kind of run loop signal or queue. If the window's object tree gets interrupted, it signals the main thread that it was interrupted and waits on a signal from the main thread with information about what to do (continue this branch or abort). The main thread then presents the document-modal window and signals the process thread after the user chooses what to do.

This may be really over-complicated for what you need, however. In that case, my recommendation would be to just go with the deprecated usage, but it really depends on your user requirements.

Orabelle answered 3/3, 2009 at 2:29 Comment(2)
Threads my ultimately be the way to go I suppose. The object tree is eventually going to get bigger and more complicated.Zea
Without seeing your app, it's obviously hard to say, but are you really sure you need threads? I have never encountered the case where putting the response in the callback method was more complex than threading the app.Lefthanded
C
2

Swift 5:

extension NSAlert {

  /// Runs this alert as a sheet.
  /// - Parameter sheetWindow: Parent window for the sheet.
  func runSheetModal(for sheetWindow: NSWindow) -> NSApplication.ModalResponse {
    beginSheetModal(for: sheetWindow, completionHandler: NSApp.stopModal(withCode:))
    return NSApp.runModal(for: sheetWindow)
  }
}
Creaturely answered 4/2, 2020 at 13:23 Comment(0)
C
1

here is my answer:

Create a global class variable 'NSInteger alertReturnStatus'

- (void)alertDidEndSheet:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
{
    [[sheet window] orderOut:self];
    // make the returnCode publicly available after closing the sheet
    alertReturnStatus = returnCode;
}


- (BOOL)testSomething
{

    if(2 != 3) {

        // Init the return value
        alertReturnStatus = -1;

        NSAlert *alert = [[[NSAlert alloc] init] autorelease];
        [alert addButtonWithTitle:@"OK"];
        [alert addButtonWithTitle:@"Cancel"];
        [alert setMessageText:NSLocalizedString(@"Warning", @"warning")];
        [alert setInformativeText:@"Press OK for OK"];
        [alert setAlertStyle:NSWarningAlertStyle];
        [alert setShowsHelp:NO];
        [alert setShowsSuppressionButton:NO];

        [alert beginSheetModalForWindow:[self window] modalDelegate:self didEndSelector:@selector(alertDidEndSheet:returnCode:contextInfo:) contextInfo:nil];

        // wait for the sheet
        NSModalSession session = [NSApp beginModalSessionForWindow:[alert window]];
        for (;;) {
            // alertReturnStatus will be set in alertDidEndSheet:returnCode:contextInfo:
            if(alertReturnStatus != -1)
                break;

            // Execute code on DefaultRunLoop
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode 
                                     beforeDate:[NSDate distantFuture]];

            // Break the run loop if sheet was closed
            if ([NSApp runModalSession:session] != NSRunContinuesResponse 
                || ![[alert window] isVisible]) 
                break;

            // Execute code on DefaultRunLoop
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode 
                                     beforeDate:[NSDate distantFuture]];

        }
        [NSApp endModalSession:session];
        [NSApp endSheet:[alert window]];

        // Check the returnCode by using the global variable alertReturnStatus
        if(alertReturnStatus == NSAlertFirstButtonReturn) {
            return YES;
        }

        return NO;
    }
    return YES;
}

Hope it'll be of some help, Cheers --Hans

Charioteer answered 18/8, 2010 at 15:48 Comment(0)
T
1

This is the version of Laurent, et al., above, translated into Swift 1.2 for Xcode 6.4 (latest working version as of today) and tested in my app. Thanks to all those who contributed to make this work! The standard documentation from Apple gave me no clues as to how go about this, at least not anywhere that I could find.

One mystery remains to me: why I had to use the double exclamation point in the final function. NSApplication.mainWindow is supposed to be just an optional NSWindow (NSWindow?), right? But the compiler gave the error shown until I used the second '!'.

extension NSAlert {
    func runModalSheetForWindow( aWindow: NSWindow ) -> Int {
        self.beginSheetModalForWindow(aWindow) { returnCode in
            NSApp.stopModalWithCode(returnCode)
        }
        let modalCode = NSApp.runModalForWindow(self.window as! NSWindow)
        return modalCode
    }

    func runModalSheet() -> Int {
        // Swift 1.2 gives the following error if only using one '!' below:
        // Value of optional type 'NSWindow?' not unwrapped; did you mean to use '!' or '?'?
        return runModalSheetForWindow(NSApp.mainWindow!!)
    }
}
Triune answered 28/7, 2015 at 21:23 Comment(1)
Brilliant, and still OK with Swift 4. The functions' return types are now NSApplication.ModalResponse and you now only need a single exclamation in the last line. Nor do we need the type-cast in line 6.Visitation
T
0

Unlike Windows I don't believe there's a way to block on modal dialogs. The input (e.g. the user clicking a button) will be processed on your main thread so there's no way of blocking.

For your task you will either have to pass the message up the stack and then continue where you left off.

Tetrastich answered 3/3, 2009 at 2:3 Comment(0)
C
0

When one object fails, stop processing the objects in the tree, make a note of which object failed (assuming that there is an order and you can pick up where you left off), and throw up the sheet. When the user dismisses the sheet, have the didEndSelector: method start processing again from the object that it left off with, or don't, depending on the returnCode.

Contraband answered 3/3, 2009 at 13:9 Comment(1)
I just re-read your question and I fear that by "I can't split the task up into two areas" you are saying that this is not possible. Sorry if my answer is not helpful.Contraband
V
0
- (bool) windowShouldClose: (id) sender
 {// printf("windowShouldClose..........\n");
  NSAlert *alert=[[NSAlert alloc ]init];
  [alert setMessageText:@"save file before closing?"];
  [alert setInformativeText:@"voorkom verlies van laatste wijzigingen"];
  [alert addButtonWithTitle:@"save"];
  [alert addButtonWithTitle:@"Quit"];
  [alert addButtonWithTitle:@"cancel"];
  [alert beginSheetModalForWindow: _window modalDelegate: self
              didEndSelector: @selector(alertDidEnd: returnCode: contextInfo:)
                 contextInfo: nil];
  return false;
}
Verdure answered 6/10, 2012 at 7:56 Comment(1)
Thanks for posting an answer! While a code snippet could answer the question it's still great to add some addition information around, like explain, etc ..Merrileemerrili
G
0

You can use dispatch_group_wait(group, DISPATCH_TIME_FOREVER);:

dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);

NSAlert *alert = [[NSAlert alloc] init];
[alert setMessageText:@"alertMessage"];
[alert addButtonWithTitle:@"Cancel"];
[alert addButtonWithTitle:@"Ok"];

dispatch_async(dispatch_get_main_queue(), ^{
    [alert beginSheetModalForWindow:progressController.window completionHandler:^(NSModalResponse returnCode) {
         if (returnCode == NSAlertSecondButtonReturn) {
             // do something when the user clicks Ok

         } else {
             // do something when the user clicks Cancel
         }

         dispatch_group_leave(group);
     }];
});

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

//you can continue your code here

Hope that helps.

Goddord answered 24/9, 2018 at 13:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.