cocoa - Espere a que[NSAlert beginSheetModalForWindow:...];
osx alerts (10)
A diferencia de Windows, no creo que haya una forma de bloquear los diálogos modales. La entrada (por ejemplo, el usuario haciendo clic en un botón) se procesará en el hilo principal, por lo que no hay forma de bloquearlo.
Para su tarea, tendrá que pasar el mensaje por la pila y continuar donde lo dejó.
Cuando muestro un NSAlert como este, obtengo la respuesta de inmediato:
int response;
NSAlert *alert = [NSAlert alertWithMessageText:... ...];
response = [alert runModal];
El problema es que esta es una aplicación modal y mi aplicación está basada en documentos. Expongo la alerta en la ventana del documento actual usando 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 hacer una pregunta confiable al usuario 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 se muestre la alerta y preguntar al usuario si continuar o cancelar (continuar procesando en la rama actual), pero dado que mi aplicación está basada en documentos, las pautas de interfaz humana de Apple determinan el uso de hojas cuando la alerta es específico de un documento.
¿Cómo puedo mostrar la hoja de alerta y esperar una 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 .: utilizo 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
Creamos una categoría en NSAlert
para ejecutar alertas de forma sincrónica , 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 está completa.
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
Cuando un objeto falla, detenga el procesamiento de los objetos en el árbol, tome nota de qué objeto falló (suponiendo que haya un pedido y pueda continuar donde lo dejó) y arroje la hoja. Cuando el usuario descarta la hoja, didEndSelector:
método didEndSelector:
comience a procesar nuevamente desde el objeto que dejó, o no, según el returnCode
.
Desafortunadamente, no hay mucho que puedas hacer aquí. Básicamente, debe tomar una decisión: vuelva a diseñar su aplicación para que pueda procesar el objeto de manera asíncrona o use la arquitectura desaprobada y no aprobada de 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. Fuera de mi cabeza, sin embargo, un par de pensamientos podrían ser:
- Procese los objetos en otro subproceso que se comunique con el subproceso principal mediante algún tipo de señal de ciclo de ejecución o cola. Si el árbol de objetos de la ventana se interrumpe, indica el hilo principal que fue interrumpido y espera una señal del hilo principal con información sobre qué hacer (continuar esta rama o abortar). Luego, el hilo principal presenta la ventana de documento-modal y señala el hilo del proceso una vez que el usuario elige qué hacer.
Sin embargo, esto puede ser muy complicado para lo que necesita. En ese caso, mi recomendación sería ir con el uso obsoleto, pero realmente depende de los requisitos de usuario.
En caso de que alguien venga a buscar esto (lo hice), lo resolví 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 sincrónicamente es tan simple como:
AlertSync* sync = [[AlertSync alloc] initWithAlert: alert asSheetForWindow: window];
int returnCode = [sync run];
[sync release];
Tenga en cuenta que existe la posibilidad de problemas de reentrada como se discutió, así que tenga cuidado si lo hace.
Esta es la versión de Laurent, et al., Arriba, traducida a Swift 1.2 para Xcode 6.4 (la última versión de trabajo a día de hoy) y probada en mi aplicación. ¡Gracias a todos los que contribuyeron para que esto funcione! La documentación estándar de Apple no me dio pistas sobre cómo hacerlo, al menos no en ninguna parte que pudiera encontrar.
Un misterio sigue siendo para mí: por qué tuve que usar el doble signo de exclamación en la función final. NSApplication.mainWindow se supone que es solo una NSWindow opcional (¿NSWindow?), ¿Verdad? Pero el compilador dio el error mostrado hasta que utilicé 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!!)
}
}
La solución es llamar
[NSApp runModalForWindow:alert];
después de beginSheetModalForWindow. Además, debe implementar un delegado que capte la acción de "diálogo cerrado" y llama [NSApp stopModal] en respuesta.
aquí está mi respuesta:
Crea 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, Cheers --Hans
- (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;
}