Search code examples
javaswingkeykey-bindingskeystroke

Map All Characters to Actions using KeyBindings


I am not new to the use of Swing, but I do a heavy amount of work with it, and I'm designing a new application. It is a drawing application. It will allow the user to click at certain places in a blank white "viewer" window, type letters and symbols using their keyboard, and thus edit the text somewhere on their viewer window.

Since I'm not using the JComponent to display this text, I need a reliable way for my application to accept input. I have chosen to use KeyBindings. But my special viewer component begins with an empty input map and empty action map.

My question. What's the simplest way to map all the letters and symbols that I can type using my keyboard to AbstractAction's using the ActionMap and InputMap? The action map needs to use WHEN_IN_FOCUSED_WINDOW to catch all input to the interface.

EDIT: I used to use a keylistener in order to do this but I failed at getting modularity the way I wanted to

I trashed the code. Threw it away completely. What I believe it said was this in SSCCE:

import java.awt.event.KeyEvent;
import static java.awt.event.KeyEvent.*;

public class MyKeysHandler extends KeyListener {
//blah blah blah
//blah blah blah
public void keyPressed(KeyEvent ke)
{
// please excuse if the boolean logic of my use of masks is off
// their proper use doesn't come to me easily
// I hope you can get the jist
    if((ke.getModifiersEx() & KeyEvent.SHIFT_DOWN_MASK) != 0) { 
        switch(ke.getKeyCode()) {
            // handle capital letters
        case VK_DELETE      : editor.handleThisSpecialKey(ke.getKeyCode);   
        case VK_BACK_SLASH  : // handle back slash
        case VK_7       : // handle the ampersand
        case VK_8       : // handle the asterisk character
        default         : // if just a normal letter...
            editor.handleThisNormalKeyPlease(KeyEvent.getKeyText(ke.getKeyCode()).toUpperCase());
        }
    }
    else {
        switch(ke.getKeyCode()) {
        case VK_DELETE      : // handle the shift-delete command
        case VK_BACK_SLASH  : // handle the question mark
        case VK_7       : // handle the 7
        case VK_8       : // handle the 8
        defualt :
            if(ke.getKeyCode() == VK_C && ke.getModifiersEX() & KeyEvent.CTRL_DOWN_MASK) != 0) {
                editor.handleTheCopyCommandPlease();    
            }   
            else 
                editor.handleThisKeyPlease(KeyEvent.getKeyText(ke.getKeyCode).toLowerCase());
        }
    }       
}
}

But this got really REALLY cumbersome. Every time you add a key you have to make sure it doesn't conflict with some other key code in the same method, and for every single keylistener you have to make for any application you have to include this meticulous "VK" switch code.

My application will also have menus, and I want to install new keybindings or accelerators (or whatever they're called - mnemonics?) for those too when I have the time. For moving text around, and for deleting them, it might just be nice to have just a few special key combos that do this. As far as key combos in what you see above? You can imagine how much of a nightmare that became for me.

I assure you without posting more code, that I try to follow good models of good reusable computer programming practice. I have a model that runs below and actually handles the editing, and a view that runs above and handles the menu button code.

Unfortunately I wish Java didn't have to be so cumbersome as what is shown above. Not all but most of my keys on my keyboard need a different response depending on what key you pressed, and combos like ctrl and shift are supported in KeyBindings.

My hope is that there is a loop solution. Then again maybe there is a solution that uses WHEN-IN-ANCESTOR and would also work. The input map I'm using is the components root pane, but I'm also open to going as far down as the viewer component itself (A JPanel) and get its InputMap instance, but I haven't done so yet because I'm experimenting.

The way that I currently access items is this: This works well for me. But it doesn't handle symbols. Maybe if I increased the span of characters in the for loop? Or manually add a few? I don't know.

import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JRootPane;
import javax.swing.KeyStroke;
public void initKeyBindingsTheEasyWay() {

JRootPane rootPane = mainPane.getRootPane();
InputMap theWIMap = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
ActionMap theAMap = rootPane.getActionMap();
for(char c = 'A'; c <= 'Z'; c++) {
    // lower case   
    KeyStroke little = KeyStroke.getKeyStroke("pressed " + c);
    theWIMap.put(little, ""+Character.toLowerCase(c));
    theAMap.put(""+Character.toLowerCase(c), new LetterAction(Character.toLowerCase(c)));
}
}

public static class LetterAction extends AbstractAction
{
    char theLetter;
    public LetterAction(char letter)
    {
        theLetter = letter;
    }

    public void actionPerformed(ActionEvent ae)
    {
        System.out.print(theLetter);
    }
}

Solution

  • I tested and tested, and finally found a solution. There are two ways to solve this problem for those interested. I won't go into the way that involved searching through API of TextComponent to discover how it was done there. That was very difficult for me to try to implement myself, and I eventually had to go another route.

    I resorted to mapping characters and virtual keys to Actions.

    This is what I was looking for: a simple way to loop through characters of interest, and thus save code and complexity in simply mapping keystrokes to actions: If you're not concerned with capturing some key I have listed below simply remove the character or virtual key from the proper array. You can even add new characters

    First instantiate a JComponent (I used a JFrame called mainPane in this example). I want to capture all keystrokes inputted into the frame, so I used JComponent.WHEN_IN_FOCUSED_WINDOW.

    note the static import of KeyEvent constants in the code below.

    import static java.awt.event.KeyEvent.*;
    
    mainPane.setFocusTraversalKeysEnabled(false);
    JRootPane rootPane = mainPane.getRootPane();
    InputMap theWIMap = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
    ActionMap theAMap = rootPane.getActionMap();
    

    Then you can add code that captures letter keystrokes...

    for(char c = 'A'; c <= 'Z'; c++) {
        // upper case
        KeyStroke capital = KeyStroke.getKeyStroke("typed " + c);
        theWIMap.put(capital, Character.toString(c));
        theAMap.put(Character.toString(c), new KeyAction(c));
    
        // lower case
        KeyStroke little = KeyStroke.getKeyStroke("typed " + Character.toLowerCase(c));
        theWIMap.put(little, Character.toString(Character.toLowerCase(c)));
        theAMap.put(Character.toString(Character.toLowerCase(c)), new KeyAction(Character.toLowerCase(c)));
    }
    

    ... then write code to capture the typing of common symbols on many keyboards (the ones you can see by looking down at the typeface on the keys on your keyboard. I have them listed here in order of their "VK constant values" as specified in Java API SE 7: Constant Field Values. (?, %, ~, and | have no VK constants).

    int[][] characters = new int[][] {
                    {'?', '%', '~', '|'},
                    {' ', ',', '-', '.', '/'},
                    {';', '=', '[', '\\', ']'}, 
                    {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'},
                    {'*', '+', ',', '-', '.', '/', '&', '*', '\"', '<', '>', '{', '}', '`', '\''},
                    {'@', ':', '^', '$', '!', '(', '#', '+', ')', '_'}
                    // if you're so inclined: add even more rows to the bottom
                    ,{'¡', '€', '\u02ff'}
    };
    
    for(int[] range : characters) 
        for(int i = 0; i < range.length; i++) {
            char charForKey = (char)range[i];
            KeyStroke keyboardKey = KeyStroke.getKeyStroke(charForKey);
            theWIMap.put(keyboardKey, charForKey);
            theAMap.put(charForKey, new KeyAction(charForKey));
        }
    

    ... and finally code to handle the typing of command keys commonly found on lots of keyboards out there.

        int[][] commands = new int[][] {
            {VK_BACK_SPACE, VK_ENTER, VK_ESCAPE},
            {VK_PAGE_UP, VK_PAGE_DOWN, VK_END, VK_HOME}, 
            {VK_LEFT, VK_UP, VK_RIGHT, VK_DOWN,  VK_DELETE},
            {VK_KP_UP, VK_KP_DOWN, VK_KP_LEFT, VK_KP_RIGHT},
        };
        for(int[] range : commands)
            for(int i = 0; i < range.length; i++) {
                KeyStroke keyboardKey = KeyStroke.getKeyStroke(range[i], 0);
                String commandForKey = KeyEvent.getKeyText(range[i]).toLowerCase();
                theWIMap.put(keyboardKey, commandForKey);
                theAMap.put(commandForKey, new KeyAction(commandForKey));
            }
    
        theWIMap.put(KeyStroke.getKeyStroke("pressed " + "TAB"), "tab");
        theAMap.put("tab", new KeyAction("tab"));
    
        theWIMap.put(KeyStroke.getKeyStroke("shift pressed " + "TAB"), "shift tab");
        theAMap.put("shift tab", new KeyAction("shift tab"));       
    

    Now add in this action code here (ignoring the part about the editor: that's the part that calls my controller):

    public class KeyAction extends AbstractAction
    {
        private static final long serialVersionUID = 1L;
        public char theLetter;
        public String theCommand;
        public enum KeyType {CHARACTER_ENTRY, KEYSTROKE_COMMAND};
        public final KeyType actionType;
    
        public KeyAction(char letter)
        {
            theLetter = letter;
            actionType = KeyType.CHARACTER_ENTRY;
        }
    
        public KeyAction(String command)
        {
            theCommand = command;
            actionType = KeyType.KEYSTROKE_COMMAND;
        }
    
    
        public void actionPerformed(ActionEvent ae)
        {
            if(actionType == KeyType.CHARACTER_ENTRY) {
                System.out.print(theLetter);
                editor.receiveKey(theLetter);
            }
            else {
                System.out.println("\n" + theCommand);
                editor.receiveCommand(theCommand);
            }
    
        }
    }
    

    And upon opening your application's GUI window, after typing something like this: "I love potato salad. [ENTER] Donate $10.00 now![TAB][SHIFT TAB]" You should see something similar to this in your terminal.

    I love potato salad.
    enter
    Donate $10.00 now!
    tab
    
    shift tab