42

I would like to capture keyevents in my little app.

What I have done:

class ViewController : NSViewController {
...
  override func keyDown(theEvent: NSEvent) {
        if theEvent.keyCode == 124 {
            println("abc")
        } else {
            println("abcd")
        }
    }

    override var acceptsFirstResponder: Bool {
        return true
    }

    override func becomeFirstResponder() -> Bool {
        return true
    }

    override func resignFirstResponder() -> Bool {
        return true
    }

...
}

What happens:

When a key pressed, the Funk sound effect plays.

I've seen many posts talking about how this is a delegate the belongs to NSView and NSViewController does not have access. But the keydown function override auto completes in a class of type NSViewController leading me to believe that this is wrong.

Cripto
  • 3,581
  • 7
  • 41
  • 65
  • Which object is the first responder? – jtbandes Sep 07 '15 at 23:00
  • 2
    To eliminate the "Funk Sound" just return nil instead of the event, in the closure. I use the localMonitor approach. – Sentry.co Jul 27 '16 at 12:00
  • 1
    One caveat muting the funk sound this way is that you don't forward the keyDown event to other parts of the app, like cmd+q, to mitigate this you could return nil only on keyDown events you want to target, if you want to target a,b,c keystrokes then return nil only on these keyStrokes etc. – Sentry.co Jul 27 '16 at 12:21
  • the signature has changed on swift 4
    override func keyDown(with event: NSEvent) {}
    – Klajd Deda Mar 19 '19 at 20:33
  • it works fine but it drops support of built-in combinations like `CMD+W` to close the window or `Ctrl+CMD+F` to maximize the window – fnc12 Jan 12 '23 at 02:18

5 Answers5

67

Xcode 8.2.1 • Swift 3.0.2

import Cocoa

class ViewController: NSViewController {

    @IBOutlet var textField: NSTextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) {
            self.flagsChanged(with: $0)
            return $0
        }
        NSEvent.addLocalMonitorForEvents(matching: .keyDown) {
            self.keyDown(with: $0)
            return $0
        }
    }
    override func keyDown(with event: NSEvent) {
        switch event.modifierFlags.intersection(.deviceIndependentFlagsMask) {
        case [.command] where event.characters == "l",
             [.command, .shift] where event.characters == "l":
            print("command-l or command-shift-l")
        default:
            break
        }
        textField.stringValue = "key = " + (event.charactersIgnoringModifiers
            ?? "")
        textField.stringValue += "\ncharacter = " + (event.characters ?? "")
    }
    override func flagsChanged(with event: NSEvent) {
        switch event.modifierFlags.intersection(.deviceIndependentFlagsMask) {
        case [.shift]:
            print("shift key is pressed")
        case [.control]:
            print("control key is pressed")
        case [.option] :
            print("option key is pressed")
        case [.command]:
            print("Command key is pressed")
        case [.control, .shift]:
            print("control-shift keys are pressed")
        case [.option, .shift]:
            print("option-shift keys are pressed")
        case [.command, .shift]:
            print("command-shift keys are pressed")
        case [.control, .option]:
            print("control-option keys are pressed")
        case [.control, .command]:
            print("control-command keys are pressed")
        case [.option, .command]:
            print("option-command keys are pressed")
        case [.shift, .control, .option]:
            print("shift-control-option keys are pressed")
        case [.shift, .control, .command]:
            print("shift-control-command keys are pressed")
        case [.control, .option, .command]:
            print("control-option-command keys are pressed")
        case [.shift, .command, .option]:
            print("shift-command-option keys are pressed")
        case [.shift, .control, .option, .command]:
            print("shift-control-option-command keys are pressed")
        default:
            print("no modifier keys are pressed")
        }
    }
}

To get rid of the purr sound when pressing the character keys you need to subclass your view, override the method performKeyEquivalent and return true.

import Cocoa

class View: NSView {
    override func performKeyEquivalent(with event: NSEvent) -> Bool {
        return true
    }
}

Sample Project

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • How can you tell if you're using swift 1.2 or 2.0 – Cripto Sep 10 '15 at 03:22
  • This works in Swift 2.x - I can get the keyDown event in my override func keyDown() event code but I still get the "Funk" sound how do you eliminate the "Funk" sound? – Neal Davis Feb 15 '16 at 21:00
  • Leo Dabus - are you suggesting I use your invisible button idea to eliminate the "Funk" sound for keyDown event? – Neal Davis Feb 15 '16 at 21:24
  • My window (view) has several "Label" fields (Labels are really just NSTextFields). – Neal Davis Feb 15 '16 at 21:27
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/103537/discussion-between-neal-davis-and-leo-dabus). – Neal Davis Feb 15 '16 at 21:32
  • @LeoDabus do you still have a sample project? – Gerwin Apr 29 '16 at 07:55
  • @LeoDabus got it! ^^ – Gerwin Apr 29 '16 at 08:05
  • This answer doesn't answer the question "how to get the controller to capture the keyboard events?" The controller should be in the responder chain somewhere - as long as other objects in the responder chain don't gobble up the event first *and* the controller can accept/become the first responder (by overriding the appropriate properties/methods) it should get the events. – wcochran Apr 21 '17 at 17:41
  • @leoDabus One Question: I try to capture "shift ArrowUp" with the function keyDown, but I don't get it. The event goes to default: . What's the correct case pattern? – mica Dec 29 '17 at 11:04
  • The flagsChanged should detect the shift press. The arrowup keycode is 126. I am not sure why it wouldn't detect that combination. Are you saying that when shift is down it doesn't print `"shift key is pressed"`? – Leo Dabus Dec 29 '17 at 15:13
  • @mica https://www.dropbox.com/s/gsg304w91dmruub/CaptureKeyDown.zip?dl=1 – Leo Dabus Dec 29 '17 at 15:29
  • You're all forgetting here that you have to unregister the monitor again after you are done! Take the return value from `addLocalMonitorForEvents` and call `removeMonitor` in `deinit`! –  Jan 27 '19 at 08:26
  • Works great, but be cautious about the strong reference inside the block, because it would cause memory leaks. – tounaobun Aug 10 '22 at 05:48
21

Swift4

Just found a solution for the very same problem, Swift4. The idea behind that: if the pressed key was handled by a custom logic, the handler shall return nil, otherwise the (unhandled) event...

class MyViewController: NSViewController {
   override func viewDidLoad() {
      super.viewDidLoad()
      // ...
      NSEvent.addLocalMonitorForEvents(matching: .keyDown) {
         if self.myKeyDown(with: $0) {
            return nil
         } else {
            return $0
         }
      }
   }
   func myKeyDown(with event: NSEvent) -> Bool {
      // handle keyDown only if current window has focus, i.e. is keyWindow
      guard let locWindow = self.view.window,
         NSApplication.shared.keyWindow === locWindow else { return false }
      switch Int( event.keyCode) {
      case kVK_Escape:
         // do what you want to do at "Escape"
         return true
      default: 
         return false
      }
   }
}

And here we are: no Purr / Funk sound when key is pressed...

[Update] Added check of keyWindow. Without this, keyDown() is fired even if another view/window contains the first responder...

Kriggel
  • 219
  • 2
  • 4
10

I was trying to find an answer for swift 3, here is what worked for me:

Swift 3

import Cocoa

// We subclass an NSView

class MainView: NSView {

    // Allow view to receive keypress (remove the purr sound)
    override var acceptsFirstResponder : Bool {
        return true
    }

    // Override the NSView keydown func to read keycode of pressed key
    override func keyDown(with theEvent: NSEvent) {
        Swift.print(theEvent.keyCode)
    }

}
Community
  • 1
  • 1
melMass
  • 3,813
  • 1
  • 31
  • 30
2

I manage to get it work from subclass of NSWindowController

class MyWindowController: NSWindowController {

    override func keyDown(theEvent: NSEvent) {
        print("keyCode is \(theEvent.keyCode)")
    }
}

UPDATE:

import Cocoa

protocol WindowControllerDelegate {
    func keyDown(aEvent: NSEvent)
}

class WindowController: NSWindowController {

    var delegate: WindowControllerDelegate?

    override func windowDidLoad() {
        super.windowDidLoad()
        delegate = window?.contentViewController as! ViewController
    }
    override func keyDown(theEvent: NSEvent) {
        delegate?.keyDown(theEvent)
    }

}

and ViewController:

class ViewController: NSViewController, WindowControllerDelegate {

    @IBOutlet weak var textField: NSTextField!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override var representedObject: AnyObject? {
        didSet {
        // Update the view, if already loaded.
        }
    }
    override func keyDown(theEvent: NSEvent) {
        textField.stringValue = "key = " + (theEvent.charactersIgnoringModifiers
            ?? "")
        textField.stringValue += "\ncharacter = " + (theEvent.characters ?? "")
        textField.stringValue += "\nmodifier = " + theEvent.modifierFlags.rawValue.description
    }

}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
Prontto
  • 1,671
  • 11
  • 21
-1
    let kLeftArrowKeyCode:  UInt16  = 123
    let kRightArrowKeyCode: UInt16  = 124
    let kDownArrowKeyCode:  UInt16  = 125
    let kUpArrowKeyCode:    UInt16  = 126
    
    override func keyDown(with event: NSEvent) {
              switch event.keyCode {
              case kLeftArrowKeyCode:
                  print("left")
                  break
              case kRightArrowKeyCode:
                  print("right")
                  break
              case kDownArrowKeyCode:
                  print("down")
                  break
              case kUpArrowKeyCode:
                  print("up")
                  break
              default:
                  print("other")
                  super.keyDown(with: event)
                  break
              }
              print("Key with number: \(event.keyCode) was pressed")
    }
AzeTech
  • 623
  • 11
  • 21