kennyc
kennyc

Reputation: 5700

Why are NSResponder's keyboard actions not being called?

NSResponder defines numerous actions that are related to keyboard actions like moveUp:, moveUpAndModifySelection: and moveToBeginningOfLine:. With the exception of the actions that aren't triggered by pressing a modifier key (e.x. moveUp: is simply the Up arrow key) I can't get any of the actions to be called on my custom NSView.

Here's the setup:

In this setup, my STView instance will correctly receive all keyboard events (including those with a modifier like Ctl-A in both performKeyEquivalent: and keyDown:, as expected. (Both methods are commented out for the discussion below.)

If I provide an implementation for moveUp:, as defined in NSResponder then moveUp: is correctly called when the user presses the Up arrow key.

Confusingly, moveUp: is also called whenever the Up arrow key is pressed with a modifier key. When a modifier key is being held (e.x.: Ctl-Up) I would expect the appropriate keyboard action method to be called.

Ex:

This pattern repeats itself for all key/modifier combinations.

Two areas in Apple's documentation seem relevant:

Reading through that documentation, it looks like the system is responsible for mapping key events to their appropriate key action. And indeed, this works for me when a modifier key is not pressed. But when a modifier key is pressed, then the event is just being handled like any other event and passed through the normal responder chain.

(I can see the modifier key event coming in through NSApplication sendEvent, though NSApplication sendAction:to:from: is never called, and continuing down the view hierarchy, which I would expect given my reading of the documentation above.

The documentation hints that text editing views might be treated differently. Are these keyboard actions only sent to views that edit text? If not, then how does one get a message like moveUpAndModifySelection: to be called on their custom NSView class when the user presses Shift-Up?

For reference, I'm using this chart for keyboard bindings, though to be frank I've tried just about every combination, listed or not:

At the end of the day, I could likely just override keyDown: and manually handle all the combinations I'm interested in, but I was hoping to at least understand why this isn't working as intended for me.

(Note that the intended application here is to allow the user to navigate a grid-like view of items, much like a collection view.)

Any tips, advice or comments would be much appreciated.

Update:

If you do the following in your keyDown

- (void)keyDown:(NSEvent *)theEvent {
  ...
  [self interpretKeyEvents:[NSArray arrayWithObject:theEvent]];
}

Then the "correct" actions will be called on your custom view. I assume this is because the actions I'm interested in are text-like actions, which require the input manager to interpret and convert to their appropriate actions.

It's still not entirely clear to me why moveUp: is properly getting called. Who in the responder chain is recognizing the Up arrow key being pressed and then sending out a moveUp: message?

Upvotes: 4

Views: 3140

Answers (2)

Ken Thomases
Ken Thomases

Reputation: 90521

This is sort of a reply to your self-answer, which is more an extension of the question.

Yes, in most cases it is necessary to call -[NSResponder interpretKeyEvents:] or -[NSTextInputContext handleEvent:] to get the key bindings system to send you the bound action methods.

As to why you're getting -moveUp:, I implemented the following in a simple custom view class:

- (BOOL) acceptsFirstResponder
{
    return YES;
}

- (void) moveUp:(id)sender
{
    NSLog(@"%@", [NSThread callStackSymbols]);
}

That logged the following call stack:

0   TestKeyUp                           0x000000010000182d -[KeyView moveUp:] + 48
1   AppKit                              0x00007fff85e44012 -[NSWindow _processKeyboardUIKey:] + 325
2   AppKit                              0x00007fff85ab1075 -[NSWindow keyDown:] + 94
3   AppKit                              0x00007fff8589e206 forwardMethod + 104
4   AppKit                              0x00007fff8589e206 forwardMethod + 104
5   AppKit                              0x00007fff8596c0c7 -[NSWindow sendEvent:] + 8769
6   AppKit                              0x00007fff858a0afa -[NSApplication sendEvent:] + 4719
7   AppKit                              0x00007fff858376de -[NSApplication run] + 474
8   AppKit                              0x00007fff858303b0 NSApplicationMain + 364
9   TestKeyUp                           0x000000010000176d main + 33
10  TestKeyUp                           0x0000000100001744 start + 52
11  ???                                 0x0000000000000001 0x0 + 1

NSWindow handles the up-arrow key as part of "keyboard interface control". This is partially documented, but it's not explicit that it will invoke methods such as -moveUp:. Apparently, that's how it's implemented.

Upvotes: 5

Abhi Beckert
Abhi Beckert

Reputation: 33349

Reading through that documentation, it looks like the system is responsible for mapping key events to their appropriate key action.

There is no "system". NSResponder works like this: an arbitrary message (just a string of characters) is dropped onto the beginning of the responder chain (whatever has keyboard focus). Usually it does nothing, and passes it along to the "next" responder. It goes along the responder chain doing nothing until after 50 or 60 null operations something finally says "yup, I'll respond to that!" and it does something. Once this happens, nothing further along the responder chain ever learns that the event occurred.

Often a responder will trigger another responder. So for example if you send a keyDown: message for Cmd-S it'll do nothing all the way up, until eventually it reaches the "save as" menu item, which then puts saveDocument: at the start of the responder chain, going down the whole thing all over again until it gets to NSDocument, which will do the save operation.

Confusingly, moveUp: is also called whenever the Up arrow key is pressed with a modifier key. When a modifier key is being held (e.x.: Ctl-Up) I would expect the appropriate keyboard action method to be called.

Ex:

  • Ctl-Up -> moveUp: (expected: scrollPageUp:)
  • Alt-Up -> moveUp: (expected: moveToBeginningOfParagraph:)
  • Shift-Up -> moveUp: (expected: moveUpAndModifySelection:)
  • Command-Up -> moveUp: (expected: moveToBeginningOfDocument:)

It's not confusing, the responder chain doesn't know anything about scroll views or text views, or anything at all. It just knows that the application pushed a keyDown or mouseDown: message onto the chain and a millisecond later a scrollPageUp: message went onto the chain, or maybe it doesn't. There are many things in the responder chain that don't ever get used. They're available to be triggered if you need them, and some things will be triggered by AppKit, but many are not.

Beware the list of methods in NSResponder is mostly just there to make life easier for developers with code auto-completion and to populate the list of available actions when building the interface with the GUI. You can put any @selector() on the responder chain (as long is at it has a single sender parameter). Not all the ones in active use are documented.

At the end of the day, I could likely just override keyDown: and manually handle all the combinations I'm interested in, but I was hoping to at least understand why this isn't working as intended for me.

Yep. If possible you should have keyDown: just do something like [self moveUpAndModifySelection:self];. So basically a big switch statement or if statement without much logic.

That's how I did it anyway: https://github.com/abhibeckert/Dux/blob/master/Dux/DuxTextView.m#L797

Upvotes: 3

Related Questions