1

I have an NSTextField in my window and 4 menu items with key equivalents .

When the text field is selected and I press an arrow key, I would expect the cursor to move in the text field but instead the corresponding menu item action is performed.

So there has to be an issue in the responder chain. To figure out what's wrong I've watched WWDC 2010 Session 145 – Key Event Handling in Cocoa Applications mentioned in this NSMenuItem KeyEquivalent space " " bug thread.


The event flow for keys (hotkeys) is shown in the session as follows:

Event Flow



So I checked the call stack with a menu item which has keyEquivalent = K (just any normal key) and for a menu item which has keyEquivalent = → (right arrow key)

K key event call stackRight arrow key event call stack

First: K key event call stack; Second: Right arrow key event call stack

So when pressing an arrow key, the event is sent directly to mainMenu.performKeyEquivalent, but it should actually be sent to the keyWindow right?

Why is that and how can I fix this behavior so that my NSTextField receives the arrow key events before the mainMenu does?

Codey
  • 1,131
  • 2
  • 15
  • 34
  • 1
    See [The Path of Key Events](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/EventArchitecture/EventArchitecture.html#//apple_ref/doc/uid/10000060i-CH3-SW10). – Willeke May 17 '20 at 02:16
  • @Willeke I've taken a look at it but I can still not figure out why I get this behavior. In the diagram it shows, if it's a keyEquivalent then it will be sent down the key windows view hierarchy. That is happening for the P shortcut but not for the arrow. According to the diagram the arrow should be sent to the firstResponder to but that doesn't happen. – Codey May 17 '20 at 07:41
  • 1
    Maybe the arrow keys are initially seen as key equivalent and a P without the Command Key modifier isn't. The call stack is not the route the event takes. – Willeke May 17 '20 at 09:54
  • Hm ok and do you have an idea how to fix that? Or how can I change that so that my textfield receives the event First? – Codey May 17 '20 at 14:22
  • Don't use the arrow keys without modifier keys as key equivalent in menu items. – Willeke May 17 '20 at 14:37
  • That is Not a very satisfactory answer :D im creating a presentation app where the user can navigate through the slides using the arrow keys. There must be another solution. Can I somehow reroute the event path? – Codey May 17 '20 at 14:43
  • There are other ways to make the arrow keys work. – Willeke May 17 '20 at 15:02
  • What would you suggest? Because I would Need to process the Event but somehow also remain the responder chain – Codey May 17 '20 at 15:05
  • 1
    See [How to handle arrow key event in Cocoa app?](https://stackoverflow.com/questions/6000133/how-to-handle-arrow-key-event-in-cocoa-app). Note: nowadays the view controller is also in the responder chain. – Willeke May 17 '20 at 15:29
  • That's seems to be the solution I will implement. But this brings another issue as it breaks my other hotkey shortcuts, since keyDown intercepts the event path. So when I don't handle the keyDown event I try to continue the path by calling (in the NSWindow) `if !self.performKeyEquivalent(with: event) { NSApp.mainMenu?.performKeyEquivalent(with: event) }`. Is this the right way to do it? – Codey May 18 '20 at 18:36
  • 1
    Call `super.keyDown(with: event)` when you don't handle the keydown. – Willeke May 18 '20 at 21:41

2 Answers2

1

Interesting observation about the call stack difference. Since arrow keys play the most important role in navigation they are probably handled differently from the rest of keys, like you saw in the NSMenuItem KeyEquivalent space " " bug thread. Again, it's one of those cases when AppKit takes care of everything behind the scenes to make your life easier in 99.9% situations.

You can see the actual difference in behaviour by pressing k while textfield has the focus. Unlike with arrows, the menu item's key equivalent doesn't get triggered and input goes directly into the control.

For your situation you can use NSMenuItemValidation protocol to override the default action of enabling or disabling a specific menu item. AFAIK this can go into any responder in a chain, e.g., view controller, window, or application. So, you can enable/disable your menu items in a single place when the window's first responder is a textfield or any other control that uses these events to properly operate.

extension ViewController: NSMenuItemValidation {
    func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
        // Filter menu item by it's assigned action, just as an exampe.
        if menuItem.action != #selector(ViewController.menuActionLeftArrowKey(_:)) { return true }
        Swift.print("Validating menu item:", menuItem)
        // Disable the menu item if first responder is text view.
        let isTextView = self.view.window?.firstResponder is NSTextView
        return !isTextView
    }
}

This will get invoked prior displaying the menu in order to update item state, prior invoking menu item key equivalent in order to check if action needs sending or not, and probably in other cases when AppKit needs to check the item's state – can't think of any from the top of my head.

P.S. Above the first responder check is done against NSTextView not NSTextField, here's why.

Ian Bytchek
  • 8,804
  • 6
  • 46
  • 72
  • 1
    Thanks a lot for your answer! This is an interesting solution. But unfortunately I can't apply this in my app, since the user needs to be able to select the menu item when editing ... – Codey May 18 '20 at 18:32
1

This is the solution I've chosen, which resulted from the comments from @Willeke.

I've created a subclass of NSWindow and overridden the keyDown(with:) method. Every Window in my application (currently 2) subclass this new NavigationWindow, so that you can use the arrow keys in every window.

class NavigationWindow: NSWindow {

    override func keyDown(with event: NSEvent) {
        if event.keyCode == 123 || event.keyCode == 126 || event.specialKey == NSEvent.SpecialKey.pageUp {
            print("navigate back")
        } else if event.keyCode == 124 || event.keyCode == 125 || event.specialKey == NSEvent.SpecialKey.pageDown {
            print("navigate forward")
        } else {
            super.keyDown(with: event)
        }
    }
}

This implementation registers all four arrow keys plus the page up and down keys for navigation.

These are the key codes

  • 123: right arrow
  • 124: left arrow
  • 125: down arrow
  • 126: up arrow
Codey
  • 1,131
  • 2
  • 15
  • 34
  • 2
    Good! One thing you might wanna use is key constants, like kVK_LeftArrow, kVK_RightArrow, etc. Most can be seen [here](https://github.com/Swifteroid/Observatory/blob/5b0a157b0feb492dafc080f79d1f2c8c8c39d812/source/Observatory/Keyboard/Keyboard.Key.swift). – Ian Bytchek May 19 '20 at 11:55