14

I have a custom NSView subclass that needs to handle some keyboard events. In Objective-C, I might handle it like this:

-(void)keyDown:(NSEvent *)event
{
    unichar ch = [[event charactersIgnoringModifiers] characterAtIndex:0];

    if (ch == NSUpArrowFunctionKey && (event.modifierFlags & NSCommandKeyMask)) {
        // Scroll to top
        return;
    }
    else if (ch == NSDownArrowFunctionKey && (event.modifierFlags & NSCommandKeyMask)) {
        // Scroll to bottom
        return;
    }

    switch (ch) {
        case NSRightArrowFunctionKey:
            // Select the current row
            return;
        case ' ':
            // Scroll down one page
            return;
        default:
            break;
    }

    [super keyDown:event];
}

In Swift, however,characterAtIndex: returns a unichar, while NSUpArrowFunctionKey: Int and " ": String (or Character). It's not clear to me how to convert a unichar to a String or Character.

I got this working, but it feels like an ugly workaround. Is there a better way?

func keyDown(theEvent: NSEvent) {
    let char = Int(theEvent.charactersIgnoringModifiers.utf16[0])  // <----- This seems ugly
    let hasCommand = (theEvent.modifierFlags & .CommandKeyMask).value != 0

    switch char {

        case NSUpArrowFunctionKey where hasCommand == true:
            // Scroll to top
            break

        case NSDownArrowFunctionKey where hasCommand == true:
            // Scroll to bottom
            break

        case NSRightArrowFunctionKey where hasCommand == true:
            // Select the current row
            break

        case Int(" ".utf16[0]):   //  <---- Surely there's a better way of doing this?
            // Scroll down one page
            break

        default:
            super.keyDown(theEvent)
    }
}
BJ Homer
  • 48,806
  • 11
  • 116
  • 129
  • Note that `theEvent.charactersIgnoringModifiers == String(NSUpArrowFunctionKey)` never returns true, as far as I can tell. – BJ Homer Jul 21 '14 at 16:46
  • Nicest thing I can come up with is to use an actual Character as the case selector: that lets you compare with `Character(" ")` for the space key, for example. You still have to do something fairly ugly to convert the integer key codes for matching, though. Best I could find was to use `Character(UnicodeScalar(NSUpArrowFunctionKey))` to turn the constants into real Swiftian Characters. Perhaps what this really needs is more Swiftian versions of the constants, rather than anything else... – Matt Gibson Jul 21 '14 at 17:56

3 Answers3

16

Let the oft-overlooked interpretKeyEvents() do the messy bits for you. It knows about all sorts of keys, including arrow keys:

override func keyDown(event: NSEvent) {
    interpretKeyEvents([event]) // calls insertText(_:), moveUp(_:), etc.
}

override func insertText(insertString: AnyObject) {
    let str = insertString as! String
    switch str {
    case " ":
        println("User hit the spacebar.")
    default:
        println("Unrecognized input: \(str)")
    }
}

override func moveUp(sender: AnyObject?) {
    println("Up arrow.")
}

override func moveLeft(sender: AnyObject?) {
    println("Left arrow.")
}

override func deleteBackward(sender: AnyObject?) {
    println("Delete.")
}

The NSResponder Class Reference section Responding to Action Messages lists these and other methods for handling keyboard events.

Adam Preble
  • 2,162
  • 17
  • 28
9

Why not use extensions?

extension NSEvent {

    var character: Int {
        // Note that you could also use Int(keyCode)
        return Int(charactersIgnoringModifiers.utf16[0])
    }

}

override func keyDown(theEvent: NSEvent!) {
    switch theEvent.character {
        case NSUpArrowFunctionKey:
            println("up!")
        case 0x20:
            println("spacebar!")
        default:
            super.mouseDown(theEvent)
    }
}

Also, note that, as stated in this answer, there is no public enum that defines all the key codes for every key. It's easiest to simply test what value it is with a println() and then use that value in the switch statement.


Edit

Or you could also extend the Character class

import Foundation
extension Character {

    var keyCode: Int {
        return Int(String(self).utf16[String.UTF16View.Index(0)])
    }

}

and test for it like this

case Character(" ").keyCode: // Spacebar
    println("spacebar!")
Community
  • 1
  • 1
IluTov
  • 6,807
  • 6
  • 41
  • 103
  • `NSEvent.keyCode` is different thing with function key unicode constants. It's a code mapped to hardware key rather than a semantic unicode character. For example, it reports `126` for `NSUpArrowFunctionKey` (defined as `0xF700` under *Function-Key Unicodes* section) in my keyboard. Don't use `keyCode` to compare function key unicode. – eonil Dec 23 '14 at 13:08
  • He's not using `NSEvent.keyCode`. The method name is a bit unfortunate, but the extension is supposed to be used with the switch statement he suggested, which matches up correctly. – nschum May 11 '15 at 08:23
4

OSX handles key-events using at least two different layers.

  • keyCode. I believe this is a code mapped to each hardware keyboard key.
  • Unicode. Predefined Unicode code-point mapped to a semantic virtual key.

You need to use Unicode key to process user events properly. You can take the Unicode code-point from the event object like this.

override func keyDown(theEvent: NSEvent) {
    let s   =   theEvent.charactersIgnoringModifiers!
    let s1  =   s.unicodeScalars
    let s2  =   s1[s1.startIndex].value
    let s3  =   Int(s2)
    switch s3 {
    case NSUpArrowFunctionKey:
        wc1.navigateUp()
        return
    case NSDownArrowFunctionKey:
        wc1.navigateDown()
        return
    default:
        break
    }
    super.keyDown(theEvent)
}

Take care that unicodeScalars is not randomly accessible, so you need to use explicit index object --- startIndex.

eonil
  • 83,476
  • 81
  • 317
  • 516