macoscocoa

NSTextField - notifications when individual keys are pressed


I am making an app that will add sound to keypresses as the user types in an NSTextField. I need to capture keystrokes and know what each individual keypress is (like "d" or "space" or "6"). The app depends on this. There is no other way around it.

Each window is an NSDocument File Owner, and it has a single NSTextField in it, which is where the document data is parsed, and the user will type.

After hours of parsing the Internet for answers and hacking away at code, the four most commonly repeated answers are:

  1. "that is not how things work, here is (irrelevant answer)"
  2. "you are new to Cocoa, that is a bad idea, use control:textView:doCommandSelector:" that doesn't give me individual keys, and some keys need their own unique sound trigger.
  3. "use controlTextDidChange: or textView:shouldChangeTextInRange:replaceString:" controlTextDidChange doesn't give me individual keys, and the second one only works for textViews or UIKit.
  4. People get confused and answer with recommendations for UIKit instead of AppKit, which is iOS-only.

The weird thing is that if I subclass NSTextField, it receives -keyUp. I don't know where -keyDown is going.

So my ultimate question is: can you tell me some kind of step-by-step way to actually capture the keyDown that is sent to NSTextField? Even if it's a hack. Even if it's a terrible idea.

I would love to solve this problem! I am very grateful for your reading.


Solution

  • This is a pretty old question, but as I was trying to implement a NSTextField that could react to keyDown so that I could create a hotkey preferences control I found I wanted the answer to this question.

    Unfortunately this is a pretty non-standard use and I didn't find any places that had a direct answer, but I've come up with something that works after digging through the documentation (albeit in Swift 4) and I wanted to post it here in case it helps someone else with a non-standard use case.

    This is largely based off of the information gleaned from the Cocoa Text Architecture Guide


    There are three components to my solution:

    1. Creating your NSWindowController and setting a NSWindowDelegate on your NSWindow:

      guard let windowController = storyboard.instanciateController(withIdentifier:NSStoryboard.SceneIdentifier("SomeSceneIdentifier")) as? NSWindowController else {
          fatalError("Error creating window controller");
      }
      if let viewController = windowController.contentViewController as? MyViewController {
          windowController.window?.delegate=viewController;
      }
      
    2. Your NSWindowDelegate

      class MyViewController: NSViewController, NSWindowDelegate {
          // The TextField you want to capture keyDown on
          var hotKeyTextField:NSTextField!;
          // Your custom TextView which will handle keyDown
          var hotKeySelectionFieldEditor:HotKeySelectionTextView = HotKeySelectionTextView();
      
          func windowWillReturnFieldEditor(_ sender: NSWindow, to client: Any?) -> Any? {
              // If the client (NSTextField) requesting the field editor is the one you want to capture key events on, return the custom field editor. Otherwise, return nil and get the default field editor.
              if let textField = client as? NSTextField, textField.identifier == hotKeyTextField.identifier {
                  return hotKeySelectionFieldEditor;
              }
              return nil;
          }
      }
      
    3. Your custom TextView where you handle keyDown

      class HotKeySelectionTextView: NSTextView {
          public override func keyDown(with event: NSEvent) {
              // Here you can capture the key presses and perhaps save state or communicate back to the ViewController with a delegate pattern if you prefer.
          }
      }
      

    I fully admit that this feels like a workaround somewhat, but as I am experimenting with Swift at the moment and not quite up to speed with all of the best practices yet I can't make an authoritative claim as to the "Swift-i-ness" of this solution, only that it does allow a NSTextField to capture keyDown events indirectly while maintaining the rest of the NSTextField functionality.