Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling keyboard events in AppKit with Swift

Tags:

swift

appkit

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)
    }
}
like image 461
BJ Homer Avatar asked Jul 21 '14 16:07

BJ Homer


3 Answers

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.

like image 179
Adam Preble Avatar answered Feb 09 '23 01:02

Adam Preble


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!")
like image 20
IluTov Avatar answered Feb 09 '23 01:02

IluTov


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.

like image 39
eonil Avatar answered Feb 08 '23 23:02

eonil