NSTextField captures return key event twice

2019-08-04 18:53发布

问题:

I have a textfield and two buttons. The user should be able to press return when they’re done editing the textfield, and then return again to activate one or the other of the buttons, depending on conditions. In order to make it clear to the user that they can return to activate the button, I temporarily assign return as the chosen button’s key equivalent, which should make it glow blue.

The textfield’s sent-action selector includes this code:

switch (self.iNavMode) {
    case kNavModeNeutral:
        break;
    case kNavModeSaveAndNew:
        [self.window makeFirstResponder:self.btnSaveAndNew];
        [self.btnSaveAndNew setKeyEquivalent:@"\r"];
        break;
    case kNavModeSaveAndNext:
        [self.window makeFirstResponder:self.btnSaveAndNext];
        [self.btnSaveAndNext setKeyEquivalent:@"\r"];
        break;
    default:
        break;
}

The chosen button’s action then knocks out the key equivalent, so that the button won’t continue to glow blue once it resigns firstResponder:

[self.btnSaveAndNext setKeyEquivalent:@""];

The problem is that when the user returns out of the textfield, the return key event is somehow captured twice, and the program activates the button on its own, even though the user has not actually pressed return again.

Is there a way that I can completely capture and dispose of the first return key event so this doesn’t happen?

回答1:

Well, I’ve got a kludge.

I added a boolean property shouldSwallowThisReturn. And I added a line that sets this boolean to yes in the textfield’s sent-action selector:

switch (self.iNavMode) {
    case kNavModeNeutral:
        break;
    case kNavModeSaveAndNew:
        [self.window makeFirstResponder:self.btnSaveAndNew];
        self.shouldSwallowThisReturn = YES;
        [self.btnSaveAndNew setKeyEquivalent:@"\r"];
        break;
    case kNavModeSaveAndNext:
        [self.window makeFirstResponder:self.btnSaveAndNext];
        self.shouldSwallowThisReturn = YES;
        [self.btnSaveAndNext setKeyEquivalent:@"\r"];
        break;
    default:
        break;
}

And I added a few lines to the chosen button’s action:

if (self.shouldSwallowThisReturn) {
    self.shouldSwallowThisReturn = NO;
    return;
}
[self.btnSaveAndNext setKeyEquivalent:@""];

So the rest of the button’s action executes only after the user has pressed return a second time.

This works, but I would prefer a more elegant solution.

Further examination of Apple’s event-handling guide suggests what’s wrong: Apparently, when you use IB to assign a sent-action to a textfield, although the action is set off when the user presses return, that return doesn’t register as a key equivalent, and therefore doesn’t respond yes to the app’s performKeyEquivalent query, and therefore the app keeps looking for a control that will respond yes, so it ends up calling the button on its own.

So it seems what I really should do is subclass the textfield and override its performKeyEquivalent method so that it returns yes if the keyCode is 36 (the code for the return key), like this:

- (BOOL) performKeyEquivalent:(NSEvent *)theEvent {
    printf("\nThe keycode is %d", [theEvent keyCode]);
    if ([theEvent keyCode] == 36) 
        return YES;
    else 
        return NO;
}

But what happens is that the override method is called even when the target textfield does not have focus. Indeed, it gets called even when the chosen button is already firstResponder. So now the user’s return is always preempted and the button’s action is never called.

I revised the override method to check the identity of the firstResponder:

- (BOOL) performKeyEquivalent:(NSEvent *)theEvent {
    printf("\nThe keycode is %d", [theEvent keyCode]);
    if ([theEvent keyCode] == 36) {
        ThisProject_AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate];
        id firstResponder = [appDelegate.windowController.window firstResponder];
        if ([firstResponder isKindOfClass:[NSTextView class]]) {
            printf("\nfirstResp is a field editor, a textview.");
            if ([firstResponder delegate] == self) {
                printf("\ntarget textfield is firstResponder.");
                return YES;
            }
        }
        else if ([firstResponder isKindOfClass:[NSButton class]]) {
            printf("\nfirstResp is a button.");
            return YES;
        }
    }
    return NO;
}

It turns out that the override is called after the execution of the textfield’s sent-action, when the firstResponder status has already been transferred to the button. So the override doesn't help.

For now, I’m stuck with the kludge at the top of this answer. But there must be some way to get a sent-action to fully capture the return key event that set it off…