3

Apple's original HIGs (now disappeared from the web site, sadly) stated that:

The rightmost button in the dialog, the action button, is the button that confirms the alert message text. The action button is usually, but not always, the default button

In my case, I have some destructive operations (such as erasing a disk) that need "safe" confirmation dialogs, like this:

"Safe" confirmation dialog

The worst option would be to make a dialog where the rightmost button would become the "do not erase" button, and the one left of it, which is usually, the Cancel button, would become the "erase" button, because that would lead easily to disaster (happened to me with a Microsoft-made dialog once), because people are trained to click the second button whenever they want to cancel an operation.

So, what I need is that the left (cancel) button becomes both the default button, and also reacts to the Return, Esc and cmd-period keys.

To make it default and also react to the Return key, I simply have to set the first button's keyEquivalent to an empty string, and the second button's to "\r".

But how to I also make the alert cancel when Esc or cmd-. are typed?

Thomas Tempelmann
  • 11,045
  • 8
  • 74
  • 149
  • It's certainly against HIG that one button responds to both ↩ and ⎋. You can't assign two different key equivalents to a button anyway. You might design a custom view and implement your own logic for handling mouse and keyboard events – vadian Nov 17 '18 at 17:05
  • Is your Cancel button actually titled "Cancel"? – Ken Thomases Nov 17 '18 at 17:35
  • @vadian I disagree. Can you show a HIG article for macOS that supports your claim? – Thomas Tempelmann Nov 17 '18 at 19:24
  • @KenThomases Yes, I forgot to mention that it's not called "Cancel", but rather "Don't Erase" or something similar. I realize that naming it Cancel might be a simply solution, perhaps. – Thomas Tempelmann Nov 17 '18 at 19:25
  • Counter question: Can you show me an example (an app) where a default button responds to both return and ESC? – vadian Nov 17 '18 at 19:41
  • Does it work to leave the Cancel button's key equivalent alone and set `alert.window.defaultButtonCell = cancelButton.cell`? – Ken Thomases Nov 17 '18 at 19:41
  • @KenThomases That has the same effect as setting `keyEquivalent` to CR - it becomes default and reacts to the Return key, but not to the Esc key. And naming it "Cancel" does also not make it react to Esc when it's made the default. – Thomas Tempelmann Nov 17 '18 at 20:27
  • @vadian The dialog above is, in fact, one from Apple, so there's your example. Also, I could never find anything in the HIG that speaks *against* doing this, and logically, it makes sense to set the default button this way in dangerous cases where a user might blindly type the Return key to continue. And the quote in my question is from the HIG, which also allows for this. Would you please elaborate why you don't agree? – Thomas Tempelmann Mar 08 '20 at 13:12
  • Of course you can assign the default key equivalent ( ↩) to the non-destructive action. But it's not possible to assign both ↩ and ⎋ as key equivalent to a button action because each action can take only **one** key equivalent. – vadian Mar 08 '20 at 14:32
  • 1
    On macOS Catalina the save dialog's file replace confirmation alert has the cancel button as default. Also the macOS guidelines state [*when a dialog may result in a destructive action, Cancel can be set as the default button.*](https://developer.apple.com/design/human-interface-guidelines/macos/windows-and-views/dialogs/) – Strom Jun 07 '20 at 17:35

1 Answers1

2

Setup the NSAlert the way you normally would, with the default button assigned. Create a new subclass of NSView with an empty bounds and add it as the NSAlert's accessory view. In the subclass's performKeyEquivalent, check for Esc and if it matches call [-NSApplication stopModalWithCode:] or [-NSWindow endSheet:returnCode:].

#import "AppDelegate.h"

@interface AlertEscHandler : NSView
@end

@implementation AlertEscHandler
-(BOOL)performKeyEquivalent:(NSEvent *)event {
    NSString *typed = event.charactersIgnoringModifiers;
    NSEventModifierFlags mods = (event.modifierFlags & NSEventModifierFlagDeviceIndependentFlagsMask);
    BOOL isCmdDown = (mods & NSEventModifierFlagCommand) != 0;
    if ((mods == 0 && event.keyCode == 53) || (isCmdDown && [typed isEqualToString:@"."])) { // ESC key or cmd-.
        [NSApp stopModalWithCode:1001]; // 1001 is the second button's response code
    }
    return [super performKeyEquivalent:event];
}
@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    [self alertTest];
    [NSApp terminate:0];
}

- (void)alertTest {
    NSAlert *alert = [NSAlert new];
    alert.messageText = @"alert msg";
    [alert addButtonWithTitle:@"OK"];
    NSButton *cancelButton = [alert addButtonWithTitle:@"Cancel"];
    alert.window.defaultButtonCell = cancelButton.cell;
    alert.accessoryView = [AlertEscHandler new];
    NSModalResponse choice = [alert runModal];
    NSLog (@"User chose button %d", (int)choice);
}
Zoë Peterson
  • 13,094
  • 2
  • 44
  • 64
  • I have posted a code example in ObjC here: https://gist.github.com/tempelmann/e9ef07f606b7cce35b3ede9ecfc4e80f - please copy that into your answer, then I'm happy to accept it fully; feel free to improve it, too. – Thomas Tempelmann Mar 08 '20 at 13:55
  • Glad I could help, and thanks for the example code. :) – Zoë Peterson Mar 09 '20 at 18:17
  • 1
    The literal codes can be replaced by constants: Import , replace 53 with `kVK_Escape`, replace 1001 with `NSAlertSecondButtonReturn`. – Martin Winter May 24 '22 at 15:57
  • 1
    Two more additions: I believe there should be a `return YES;` after stopping the modal dialog (inside the `if`). Also, just for the record, if the alert was presented as a sheet, the call to `+[NSApp stopModalWithCode:]` should be replaced with this call: `[self.window.sheetParent endSheet:self.window returnCode:NSAlertSecondButtonReturn];` – Martin Winter May 24 '22 at 20:46