Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2015-03-30

Key Bindings, NSResponder, keyDown, etc

No Swift subject this time. But sooner or later a developer that implements an editor of some kind will likely run into the topic of key-bindings.

Simply stated, key bindings map key strokes to function calls on the Responder chain. For the rest of this post I will assume familiarity with the responder chain operations.

The responder chain has one particular function 'keyDown:' that determines how an application responds to key events.

    override func keyDown(theEvent: NSEvent) {
        interpretKeyEvents([theEvent])

    }

The default implementation is to call another operation, called 'interpretKeyEvents:' which I believe to do the actual work of finding out which key should be mapped to which function. Of course you can interpret all the key's you need yourself and only call the interpretKeyEvents only for those key's you do not need (or ignore those key's altogether - at your own risk!)

It is probably necessary to take a little step backward: except for the general 'keyDown' function, there are a host of other key-related functions. Just for fun, here is a partial list containing the functions that are related to text editing:

    /************************* Standard bindable commands *************************/
    func moveForward(sender: AnyObject?)
    func moveRight(sender: AnyObject?)
    func moveBackward(sender: AnyObject?)
    func moveLeft(sender: AnyObject?)
    func moveUp(sender: AnyObject?)
    func moveDown(sender: AnyObject?)
    func moveWordForward(sender: AnyObject?)
    func moveWordBackward(sender: AnyObject?)
    func moveToBeginningOfLine(sender: AnyObject?)
    func moveToEndOfLine(sender: AnyObject?)
    func moveToBeginningOfParagraph(sender: AnyObject?)
    func moveToEndOfParagraph(sender: AnyObject?)
    func moveToEndOfDocument(sender: AnyObject?)
    func moveToBeginningOfDocument(sender: AnyObject?)
    func pageDown(sender: AnyObject?)
    func pageUp(sender: AnyObject?)
    func centerSelectionInVisibleArea(sender: AnyObject?)
    
    func moveBackwardAndModifySelection(sender: AnyObject?)
    func moveForwardAndModifySelection(sender: AnyObject?)
    func moveWordForwardAndModifySelection(sender: AnyObject?)
    func moveWordBackwardAndModifySelection(sender: AnyObject?)
    func moveUpAndModifySelection(sender: AnyObject?)
    func moveDownAndModifySelection(sender: AnyObject?)
    
    func moveToBeginningOfLineAndModifySelection(sender: AnyObject?)
    func moveToEndOfLineAndModifySelection(sender: AnyObject?)
    func moveToBeginningOfParagraphAndModifySelection(sender: AnyObject?)
    func moveToEndOfParagraphAndModifySelection(sender: AnyObject?)
    func moveToEndOfDocumentAndModifySelection(sender: AnyObject?)
    func moveToBeginningOfDocumentAndModifySelection(sender: AnyObject?)
    func pageDownAndModifySelection(sender: AnyObject?)
    func pageUpAndModifySelection(sender: AnyObject?)
    func moveParagraphForwardAndModifySelection(sender: AnyObject?)
    func moveParagraphBackwardAndModifySelection(sender: AnyObject?)
    
    func moveWordRight(sender: AnyObject?)
    func moveWordLeft(sender: AnyObject?)
    func moveRightAndModifySelection(sender: AnyObject?)
    func moveLeftAndModifySelection(sender: AnyObject?)
    func moveWordRightAndModifySelection(sender: AnyObject?)
    func moveWordLeftAndModifySelection(sender: AnyObject?)
    
    func moveToLeftEndOfLine(sender: AnyObject?)
    func moveToRightEndOfLine(sender: AnyObject?)
    func moveToLeftEndOfLineAndModifySelection(sender: AnyObject?)
    func moveToRightEndOfLineAndModifySelection(sender: AnyObject?)
    
    func scrollPageUp(sender: AnyObject?)
    func scrollPageDown(sender: AnyObject?)
    func scrollLineUp(sender: AnyObject?)
    func scrollLineDown(sender: AnyObject?)
    
    func scrollToBeginningOfDocument(sender: AnyObject?)
    func scrollToEndOfDocument(sender: AnyObject?)
    
    /* Graphical Element transposition */
    
    func transpose(sender: AnyObject?)
    func transposeWords(sender: AnyObject?)
    
    /* Selections */
    
    func selectAll(sender: AnyObject?)
    func selectParagraph(sender: AnyObject?)
    func selectLine(sender: AnyObject?)
    func selectWord(sender: AnyObject?)
    
    /* Insertions and Indentations */
    
    func indent(sender: AnyObject?)
    func insertTab(sender: AnyObject?)
    func insertBacktab(sender: AnyObject?)
    func insertNewline(sender: AnyObject?)
    func insertParagraphSeparator(sender: AnyObject?)
    func insertNewlineIgnoringFieldEditor(sender: AnyObject?)
    func insertTabIgnoringFieldEditor(sender: AnyObject?)
    func insertLineBreak(sender: AnyObject?)
    func insertContainerBreak(sender: AnyObject?)
    func insertSingleQuoteIgnoringSubstitution(sender: AnyObject?)
    func insertDoubleQuoteIgnoringSubstitution(sender: AnyObject?)
    
    /* Case changes */
    
    func changeCaseOfLetter(sender: AnyObject?)
    func uppercaseWord(sender: AnyObject?)
    func lowercaseWord(sender: AnyObject?)
    func capitalizeWord(sender: AnyObject?)
    
    /* Deletions */
    
    func deleteForward(sender: AnyObject?)
    func deleteBackward(sender: AnyObject?)
    func deleteBackwardByDecomposingPreviousCharacter(sender: AnyObject?)
    func deleteWordForward(sender: AnyObject?)
    func deleteWordBackward(sender: AnyObject?)
    func deleteToBeginningOfLine(sender: AnyObject?)
    func deleteToEndOfLine(sender: AnyObject?)
    func deleteToBeginningOfParagraph(sender: AnyObject?)
    func deleteToEndOfParagraph(sender: AnyObject?)
    
    func yank(sender: AnyObject?)
    
    /* Completion */
    
    func complete(sender: AnyObject?)
    
    /* Mark/Point manipulation */
    
    func setMark(sender: AnyObject?)
    func deleteToMark(sender: AnyObject?)
    func selectToMark(sender: AnyObject?)

    func swapWithMark(sender: AnyObject?)

You can find this list in the definition of NSResponder.h/swift. Other than fun, you can see from this list that building an editor is a lot of work. Determining which key's are mapped to which function is also a lot of work. Work that can be done by 'interpretKeyEvents:' instead of rolling our own.

Another advantage of using 'interpretKeyEvents:' is that the user gains a level of control over which key stroke (series) is mapped to which function.

By default the standard keybindings are specified in the file:
/System/Library/Frameworks/AppKit.framework/Resources/StandardKeyBinding.dict

That file can only be changed by a administrator of course, but there is another optional keybindings file:
~/Library/KeyBindings/DefaultKeyBinding.dict
this file can be changed by the user. Normally this file is not even present (it is optional after all). Many users will likely never know about this, and that may be a good thing. But power users will probably want to create and tweak this file to achieve optimal personal performance. However the mappings in this file are the same for all applications. This makes sense as a user will not want to learn different key mappings for different applications. For us as developers this means that we should stay with the list of functions (partly shown above) and pick and choose to override those that we need. The key mappings are then completely under user control - even if he/she never uses it.

Btw: this link https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/EventOverview/TextDefaultsBindings/TextDefaultsBindings.html#//apple_ref/doc/uid/20000468-CJBDEADF shows more about the key bindings file, its makeup etc. When the link no longer works, do a search for "Text System Defaults and Key Bindings". That should get you to the most recent location of that information.

Conclusion: Of the first responder functions, implement the 'keyDown:' and call 'interpretKeyEvents:' on self. Then pick and choose from the editing functions for implementation. Mapping of keys to functions should be left to the user. You can help your user by implementing as many of the editing functions as possible, even the ones that are not mapped by default.

Happy coding

Addendum: Also see Key Bindings, part 2

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

No comments:

Post a Comment