Pregunta

Cuando muestro un NSAlert como este, recibo la respuesta de inmediato:

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

El problema es que esto es modal de la aplicación y mi aplicación está basada en documentos. Muestro la alerta en la ventana del documento actual utilizando hojas, como esta:

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;
}

El único problema con esto es que beginSheetModalForWindow: regresa de inmediato, por lo que no puedo hacerle una pregunta al usuario de manera confiable y esperar una respuesta. Esto no sería un gran problema si pudiera dividir la tarea en dos áreas pero no puedo.

Tengo un bucle que procesa unos 40 objetos diferentes (que están en un árbol). Si un objeto falla, quiero que la alerta muestre y pregunte al usuario si debe continuar o abortar (continuar con el procesamiento en la rama actual), pero como mi aplicación está basada en documentos, las Pautas de Interfaz Humana de Apple dictan el uso de hojas cuando la alerta está activada. específico de un documento.

¿Cómo puedo mostrar la hoja de alerta y esperar una respuesta?

¿Fue útil?

Solución

Lamentablemente, no hay mucho que puedas hacer aquí. Básicamente, tiene que tomar una decisión: vuelva a diseñar su aplicación para que pueda procesar el objeto de forma asíncrona o use la arquitectura obsoleta y no aprobada para presentar las alertas modales de la aplicación.

Sin conocer ninguna información sobre su diseño real y cómo procesa estos objetos, es difícil proporcionar más información. Sin embargo, en la parte superior de mi cabeza, un par de pensamientos podrían ser:

  • Procese los objetos en otro hilo que se comunica con el hilo principal a través de algún tipo de señal o cola de bucle de ejecución. Si el árbol de objetos de la ventana se interrumpe, señala al hilo principal que se interrumpió y espera una señal desde el hilo principal con información sobre qué hacer (continuar con esta rama o cancelar). El hilo principal luego presenta la ventana de documento modal y señala el hilo de proceso después de que el usuario elija qué hacer.

Sin embargo, esto puede ser demasiado complicado para lo que necesitas. En ese caso, mi recomendación sería simplemente ir con el uso en desuso, pero realmente depende de sus requisitos de usuario.

Otros consejos

Creamos una categoría en NSAlert para ejecutar alertas de forma sincronizada , al igual que los diálogos de aplicación modal:

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];

El código está disponible a través de GitHub , y la versión actual publicada a continuación para completar la información.


Archivo de encabezado NSAlert + SynchronousSheet.h :

#import <Cocoa/Cocoa.h>


@interface NSAlert (SynchronousSheet)

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

@end

Archivo de implementación 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

La solución es llamar

[NSApp runModalForWindow:alert];

después de beginSheetModalForWindow. Además, debe implementar un delegado que detecte el diálogo " se ha cerrado " acción, y llama a [NSApp stopModal] en respuesta.

Aquí hay una categoría de NSAlert que resuelve el problema (como sugirió Philipp con la solución propuesta por Frederick y mejorada por Laurent P .: uso un bloque de código en lugar de un delegado, por lo que se simplifica una vez más).

@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

En caso de que alguien venga a buscar esto (yo lo hice), resolví esto con lo siguiente:

@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

Luego, ejecutar un NSAlert de forma síncrona es tan simple como:

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

Tenga en cuenta que existe el potencial de problemas de reingreso según lo discutido, así que tenga cuidado al hacer esto.

aquí está mi respuesta:

Cree una variable de clase global '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;
}

Espero que sea de alguna ayuda, Aclamaciones --Hans

Esta es la versión de Laurent, et al., arriba, traducida a Swift 1.2 para Xcode 6.4 (la última versión en funcionamiento a partir de hoy) y probada en mi aplicación. ¡Gracias a todos los que contribuyeron para hacer este trabajo! La documentación estándar de Apple no me dio ninguna pista sobre cómo hacerlo, al menos no en ningún lugar que pudiera encontrar.

Me queda un misterio: por qué tuve que usar el signo de exclamación doble en la función final. Se supone que NSApplication.mainWindow es solo una ventana opcional de NS (NSWindow), ¿verdad? Pero el compilador dio el error mostrado hasta que usé el segundo '!'.

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!!)
    }
}

A diferencia de Windows, no creo que haya una forma de bloquear los diálogos modales. La entrada (por ejemplo, el usuario que hace clic en un botón) se procesará en el subproceso principal para que no haya forma de bloquear.

Para tu tarea, tendrás que pasar el mensaje por la pila y luego continuar donde lo dejaste.

Cuando un objeto falla, deja de procesar los objetos en el árbol, toma nota de qué objeto falló (asumiendo que hay una orden y puedes continuar donde lo dejaste) y arroja la hoja. Cuando el usuario descargue la hoja, haga que el método didEndSelector: comience nuevamente a procesar el objeto que dejó, o no, según el returnCode .

- (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;
}

Puedes usar dispatch_group_wait (grupo, 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

Espero que ayude.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top