Keyboard Bindings in Swing

This document provides a review of the existing key binding facilities in Swing and defines a new set of APIs that unify what we have today and satisfy the requirements listed in the next section.

Requirements for the Keyboard Binding Infrastructure

This is a list of all of the requirements we've tried to satisfy, roughly in priority order.

  • Easy to read. It should be easy to find the binding for control-shift-A in the source code, even if there are 100 bindings defined by the ComponentUI class. This implies that a table of bindings shouldn't be tangled up with boilerplate.
  • Each ComponentUI subclass should provide a table of the actions it defines. This list could also be exposed by JComponent.
  • All actions should have a name, and a short description. The AbstractAction class allows for this. The name can be used when defining a key binding. This documents the action for the sake of future Swing developers, for generating javadoc, and for IDE users.
  • Similarly, the bindings defined for a ComponentUI subclass should be exposed in ComponentUI and in JComponent. If we provide access to the list of actions and the list of bindings, building a customizer (or whatever) that allows one to change the bindings or add new ones would be straightforward.
  • If we're going to allow developers to easily customize the keyboard bindings we should ensure that it's easy to restore the default bindings for a component.
  • Self documenting. It should be straightforward to generated a tidy HTML table that shows the mapping from keys to actions. Ideally one should be able to do this without creating an instance of the component whose bindings you'd like to document.
  • Extensible. It should be possible to define a new first class action without changing the ComponentUI API. For example a subclass of JList should be able to create a set of generic actions that would be visible in builders in the same way that the actions provided by the ListUI class are.
  • JDK 1.2 Keyboard Bindings Infrastructure

    There are two mechanisms for creating keyboard bindings in Swing 1.1: the JComponent registerKeyboardAction methods and the Keymap support in the text classes. Both approaches use KeyStroke and Action objects to characterize a binding. On the whole they're more the same than they are different. Here's a quick review of each one.

    JComponent.registerKeyboardAction()

    The JComponent class supports managing keyboard bindings with a set of public methods that add and remove entries in a private table:

        void registerKeyboardAction(ActionListener a, String command, KeyStroke k, int condition)
        void registerKeyboardAction(ActionListener a, KeyStroke k, int condition)
        void unregisterKeyboardAction(KeyStroke k)
        void resetKeyboardActions()
                                

    The registerKeyboardAction methods add an entry to the table that means: "when KeyStroke k occurs, invoke a.actionPerformed(). The actionPerformed method is passed an ActionEvent whose source is the component and whose actionCommand is the specified command string. The shorter version of registerKeyboardAction just uses null for command . The condition allows one to specify when the binding is valid:

    The unregisterKeyboardAction and resetKeyboardActions methods support removing one binding or all of them. In addition to the methods for managing the binding table, there are a few public JComponent methods for reading the table:

        KeyStroke[] getRegisteredKeyStrokes()
        int getConditionForKeyStroke(KeyStroke aKeyStroke)
        ActionListener getActionForKeyStroke(KeyStroke aKeyStroke)
                                

    Keystroke processing is driven by JComponent.processKeyEvent . If an incoming KeyEvent isn't consumed by the FocusManager or any of the components KeyListeners, the action associated with the KeyEvent is retrieved from the binding table (which is stored as a semi-private client property under "_KeyboardBindings").

    All of the ComponentUI subclasses use registerKeyboardAction to enable keyboard navigation, except for the text classes. Conventionally, each BasicXXXUI subclass has a pair of methods, installKeyboardActions() and uninstallKeyboardActions() that use registerKeyboardAction and unregister KeyboardAction respectively to manage the bindings. The code that does this is largely boilerplate and can be difficult to read in large doses. For example here's just a little of installKeyboardActions() in BasicListUI:

    	// page up
    	list.registerKeyboardAction(new PageUpAction
    		        ("SelectPageUp", CHANGE_SELECTION),
    			KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0),
    			JComponent.WHEN_FOCUSED);
    	list.registerKeyboardAction(new PageUpAction
    		        ("ExtendSelectPageUp", EXTEND_SELECTION),
    			KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, InputEvent.
    			SHIFT_MASK), JComponent.WHEN_FOCUSED);
    
    	// page down
    	list.registerKeyboardAction(new PageDownAction
    		        ("SelectPageDown", CHANGE_SELECTION),
    			KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0),
    			JComponent.WHEN_FOCUSED);
    	list.registerKeyboardAction(new PageDownAction
    		        ("ExtendSelectPageDown", EXTEND_SELECTION),
    			KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN,
    			InputEvent.SHIFT_MASK), JComponent.WHEN_FOCUSED);
                                

    [TBD: note the design flaw in the current archicture: menu items, with their dubious parentage, can't really use WHEN_IN_FOCUSED_WINDOW.]

    The Text Package and Keymaps

    JTextComponent, the superclass for the Swing text components, uses an ordered list of named Keymaps to define key bindings. Each text component has a default keymap that contains generic key bindings. The TextUI classes insert look and feel specific Keymaps in front of the default one. The default keymap, which is the tail of the keymaps list, is the value of the JTextComponent "keymap" property:

        void setKeymap(Keymap map)
        Keymap getKeymap()
                                

    The following JTextComponent static utility methods support managing Keymap lists. The addKeymap method inserts an empty keymap named name before parent, removeKeymap removes a keymap from the list, and getKeymap looks one up by name.

        static Keymap addKeymap(String name, Keymap parent)
        static Keymap removeKeymap(String name)
        static Keymap getKeymap(String name)
                                

    The Keymap class provides a nice set of methods for managing bindings, e.g. the add, remove, and lookup operations are:

        void addActionForKeyStroke(KeyStroke key, Action a)
        void removeKeyStrokeBinding(KeyStroke key);
        Action getAction(KeyStroke key);
                                

    The text components handle pressed/released KeyEvents by finding the first Keymap with a non-null binding for the corresponding KeyStroke. If the bindings Action is enabled, it's actionPerformed method is applied to an ActionEvent whose actionCommand property is the (string value of the) key character that matched the binding.

    KeyMaps also have a defaultAction property that's used for KeyEvent.KEY_TYPED events that don't have an ordinary binding. [Why is this property called "defaultAction"?]

    The text components also support a read-only property called "actions" whose value is a complete list of the actions supported by the Component. Both the generic actions and the look and feel specific actions are combined to form this list.

    The Text UI classes store a simple version of each Look and Feel specific Keymap in an array of KeyBinding objects in the defaults table. The KeyBindings are combined with the Actions defined by the UI class to create a Keymap. The swing LookAndFeel class provides a utility method for creating KeyBinding arrays from a list of Strings. The result is relatively easy to read.

    Incompatibilities

    There are two serious incompatabilities between the JComponent registerKeyboardAction machinery and the Keymap based system in the text package:

    JDK 1.3 Keyboard Bindings Infrastructure

    In Kestrel we'll replace the two existing keyboard binding systems with a new API that unifies them and satisfies the goals listed in the first section. The unified API is based on two new classes: InputMap and ActionMap. Both of these classes are just simple tables or "maps". An InputMap maps a KeyStroke to an object and ActionMap maps from an object to an Action. In Kestrel Swing will handle incoming key events with a simple three step process:

    Object actionMapKey = inputMap.get(KeyStroke.getKeyStroke(keyEvent));
    if (actionMapKey != null) {
        Action action = actionMap.get(actionMapKey);
        if (action != null) {
    	// run the actions actionPerformed() method
        }
    }
        

    Incoming KeyEvents are converted to KeyStroke objects, KeyStrokes are mapped by the components InputMap to an object that's used as a key for the ActionMap. If a non-null entry in the ActionMap is found, the actions actionPerformed method is invoked.

    The keybinding infrastructure is slightly more complicated than the description above implies because of the need to support component key bindings that apply even when the component itself doesn't have the focus. The existing keybinding infrastructure defines two other important scopes for a key binding: when a descendant of the component has the focus and when the component is a descendant of a top level window that has the focus. Each kind of keybinding gets it's own InputMap.

    All Swing components, i.e. all subclasses of JComponent, will have an ActionMap and three InputMaps, one for each keybinding scope. A components ActionMap is initialized by it's UI (its look and feel implementation) with both generic actions and look and feel specific actions. The three InputMaps are initialized similarly and they provide support for the three kinds of keyboard bindings the existing registerKeyboardAction method supports: WHEN_FOCUSED, WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, WHEN_IN_FOCUSED_WINDOW.

    InputMaps and ActionMaps have a parent property whose value is another map of the same type or null. In both classes the lookup method, get(key), recursively searches the parent map if a local match isn't found. This enables sharing, e.g. most text components can share a single InputMap that contains basic bindings for caret motion, character editing, cut and paste, and so on. To protect developers from inadvertantly changing values in shared maps, the value of the JComponent InputMap and ActionMap properties is always map whose parent is the potentially shared map provided by the UI. The map itself is initially empty.

    The fact that the getInputMap method returns an empty component-local map makes adding a new binding for an existing action simple. For example to bind the F10 key to the "cut" action in myComponent one would write:

    myComponent.getInputMap().put(KeyStroke.getKeyStroke("F10"), "cut");
        

    There's no need to create a new InputMap and configure its parent and set the inputMap property. To defeat the binding for an existing keystroke we bind the KeyStroke to an action called "none", which is never bound to an action by convention. In the following example we've defeated the binding for he Win32 style cut accelerator "control-C":

    myComponent.getInputMap().put(KeyStroke.getKeyStroke("control C"), "none");
        

    New actions can be added to a component equally easily. The conventional key for an action is it's name, here's an example:

    Action myAction = new AbstractAction("doSomething") {
        public void actionPerformed() {
    	doSomething();
        }
    };
    myComponent.getActionMap().put(myAction.get(Action.NAME), myAction);
        

    The following sections itemize the Swing API that has been changed or added to support the new key binding infrastructure.

    The InputMap Class

    InputMap associates a KeyStroke with an Object (usually the name of the action as a String, but that is up to you). InputMap, is esentially a strongly typed version of the Map interface defined in the collection classes. InputMap also has a parent property of type InputMap. InputMap is implemented such that if a binding is asked for (using the get method) that is not contained in the receiver, the parent InputMap is invoked. The following code creates two InputMaps, one a parent of the other:

        child = new InputMap();
        child.put(KeyStroke.getKeyStroke('a'), "A");
        parent = new InputMap();
        parent.put(InputMap.getKeyStroke('b'), "B");
        child.setParent(parent);
    

    child does not have a binding for 'b', but its parent does. So that child.get(KeyStroke.getKeyStroke('b')) will return "B".

    Here's the InputMap API.

    /**
     * <code>InputMap</code> provides a binding between an input event
     * (currently only <code>KeyStroke</code>s are used)
     * and an <code>Object</code>. <code>InputMap</code>s
     * are usually used with an <code>ActionMap</code>,
     * to determine an <code>Action</code> to perform
     * when a key is pressed.
     * An <code>InputMap</code> can have a parent
     * that is searched for bindings not defined in the <code>InputMap</code>.
     *
     * @version 1.5 06/03/99
     * @author Scott Violet
     * @since 1.3
     */
    public class InputMap implements Serializable {
    
        /** 
         * Creates an <code>InputMap</code> with no parent and no mappings.
         */
        public InputMap()
    
        /**
         * Sets this <code>InputMap</code>'s parent.
         *
         * @param map  the <code>InputMap</code> that is the parent of this one
         */
        public void setParent(InputMap map) 
    
        /**
         * Gets this <code>InputMap</code>'s parent.
         *
         * @return map  the <code>InputMap</code> that is the parent of this one,
         *              or null if this <code>InputMap</code> has no parent
         */
        public InputMap getParent()
    
        /**
         * Adds a binding for <code>keyStroke</code> to <code>actionMapKey</code>.
         * If <code>actionMapKey</code> is null, this removes the current binding
         * for <code>keyStroke</code>.
         */
        public void put(KeyStroke keyStroke, Object actionMapKey)
    
        /**
         * Returns the binding for <code>keyStroke</code>, messaging the 
         * parent <code>InputMap</code> if the binding is not locally defined.
         */
        public Object get(KeyStroke keyStroke) 
    
        /**
         * Removes the binding for <code>key</code> from this
         * <code>InputMap</code>.
         */
        public void remove(KeyStroke key)
    
        /**
         * Removes all the mappings from this <code>InputMap</code>.
         */
        public void clear() 
    
        /**
         * Returns the <code>KeyStroke</code>s that are bound in this <code>InputMap</code>.
         */
        public KeyStroke[] keys()
    
        /**
         * Returns the number of <code>KeyStroke</code> bindings.
         */
        public int size() 
    
        /**
         * Returns an array of the <code>KeyStroke</code>s defined in this 
         * <code>InputMap</code> and its parent. This differs from <code>keys()</code> in that
         * this method includes the keys defined in the parent.
         */
        public KeyStroke[] allKeys()
    </pre>
    
        

    The ActionMap Class

    ActionMap associates an Object (usually the name of the action as a String) to an Action. ActionMap is esentially a InputMap, with Object replacing KeyStroke, and Action replacing Object. ActionMap also has a parent property that behaves in the same way as the parent property in InputMap.

    Here's the ActionMap API:

    /**
     * ActionMap provides mappings from
     * Objects
     * (called keys or Action names)
     * to Actions.
     * An ActionMap is usually used with an InputMap
     * to locate a particular action
     * when a key is pressed. As with InputMap,
     * an ActionMap can have a parent 
     * that is searched for keys not defined in the ActionMap.
     *
     * @version 1.4 06/03/99
     * @author Scott Violet
     * @since 1.3
     */
    public class ActionMap implements Serializable {
        /**
         * Creates an ActionMap with no parent and no mappings.
         */
        public ActionMap() 
    
        /**
         * Sets this ActionMap's parent.
         * 
         * @param map  the ActionMap that is the parent of this one
         */
        public void setParent(ActionMap map) 
    
        /**
         * Returns this ActionMap's parent.
         * 
         * @return the ActionMap that is the parent of this one,
         * 	       or null if this ActionMap has no parent
         */
        public ActionMap getParent() 
    
        /**
         * Adds a binding for key to action.
         * If action is null, this removes the current binding
         * for key.
         * 

    In most instances, key will be * action.getValue(NAME). */ public void put(Object key, Action action) /** * Returns the binding for key, messaging the * parent ActionMap if the binding is not locally defined. */ public Action get(Object key) /** * Removes the binding for key from this ActionMap. */ public void remove(Object key) /** * Removes all the mappings from this ActionMap. */ public void clear() /** * Returns the Action names that are bound in this ActionMap. */ public Object[] keys() /** * Returns the number of KeyStroke bindings. */ public int size() /** * Returns an array of the keys defined in this ActionMap and * its parent. This method differs from keys() in that * this method includes the keys defined in the parent. */ public Object[] allKeys() }

    The Implementation and Related Changes

    The plan is to move to using InputMap and ActionMap in lieu of registerKeyboardAction. registerKeyboardAction, in all incarnations, will be deprecated. JComponent will get the following methods:

        public final void setInputMap(int, InputMap);
        public final InputMap getInputMap(int);
        public setActionMap(ActionMap);
        public ActionMap getActionMap();
    

    The integer passed to set/getInputMap will be one of: WHEN_IN_FOCUSED_WINDOW, WHEN_ANCESTOR_OF_FOCUSED_COMPONENT or WHEN_IN_FOCUSED_WINDOW , implying JComponent maintains three InputMaps as well as the ActionMap. There is a single ActionMap containing all the actions that the InputMaps reference.

    Each ComponentUI will have the opportunity to provide an ActionMap, as well as three InputMaps for the conditions. The InputMaps and ActionMap provided by the UI will implement the UIResource interface (use javax.swing.plaf.InputMapUIResource and javax.swing.plaf.ActionMapUIResource). The InputMaps provided by the UI will be set as the parent of the InputMap (actually, as the parent of the first InputMap with a null parent) contained in the JComponent, the ActionMap will be set in a similiar manner. SwingUtilties will provide the convenience methods replaceUIActionMap and replaceUIInputMap to manipulate the UI InputMap/ActionMap, as well as the methods getUIActionMap and getUIInputMap to obtain the Maps provided by the UI. Installing a UI InputMap looks like:

        SwingUtilties.replaceUIInputMap(jcomponent, int, keyMap);
    

    And to remove the UI InputMap use the following:

        SwingUtilties.replaceUIInputMap(component, int, null);
    

    ComponentInputMap and WHEN_IN_FOCUSED_WINDOW?

    WHEN_IN_FOCUSED_WINDOW bindings are handled very differently from the rest of the bindings. They are handled differently to avoid having to walk the container hierarchy each time a KeyEvent goes unconsumed by normaly processing (thank Steve for this, originally the container hieararchy was walked each time leading to serious performance degredation when typing!). To facilitate this, ComponentInputMap is used for all WHEN_IN_FOCUSED_WINDOW bindings. ComponentInputMap extends InputMap, adding an associated JComponent property. When the InputMap is modified, the JComponent is notfied so that it can update its internal state. Further, ComponentInputMaps only allow parents of type ComponentInputMap.

    The astute reader may have surmised that ComponentInputMap's can not be shared. Since a ComponentInputMap is associated with a single JComponent it should not be shared (in reality it can be shared, but changes to the InputMap may not be picked up by the associated components). While this may seem nasty, we believe that WHEN_IN_FOCUSED_WINDOW bindings are seldomly used.

    Processing of KeyEvents

    There are a number of entry points into the new system allowing the developer to override behavior in a number of ways. The following is a listing of how KeyEvents are processed (the processing stops when the event is consumed):

    1. The initial entry point for JComponent is processKeyEvent, which is invoked as part of the AWT processing of events (JComponent overrides this method this method defined in Component). processKeyEvent is only invoked if KeyEvents are enabled (JComponent will do this if it has a valid InputMap) or a KeyListener has been added. If a developer requests focus on a Component that does not have any bindings (or does not descend from JComponent) any WHEN_ANCESTOR_OF_FOCUSED_COMPONENT or WHEN_IN_FOCUSED_WINDOW bindings will not work!
    2. The FocusManager is handed the event, if the FocusManager does something as a result of the action (say, changes focus) it will consume the event.
    3. Any registered KeyListeners are notified by invoking super.processKeyEvent.
    4. processComponentKeyEvent is invoked to allow any customizations in a subclasses to happen.
    5. If the event represents a KEY_RELEASED event, and a corresponding KEY_PRESSED event was not received processing will stop (it is likely this code is the result of a bug that existed in the AWT, we need to investigate if this is still needed).
    6. The WHEN_FOCUSED InputMap is checked (by invoking the protected method processKeyBinding with a condition == WHEN_FOCUSED). If there is a binding, the action is enabled, and the component is enabled actionPerformed is invoked on the Action and the event is consumed.
    7. The container hiearchy is walked from the focused component to the Window, Applet or JInternalFrame invoking processKeyBinding with a condition == WHEN_ANCESTOR_OF_FOCUSED_COMPONENT. Similiar to the previous step, if a binding exists, the receiver is enabled and the action is enabled the action is notified and the event is consumed.
    8. WHEN_IN_FOCUSED_WINDOW bindings are checked by way of the KeyboardManager. The KeyboardManager is a package private class that maintains a mapping from top level components to a Hashtable mapping from KeyStroke to components. If there is a component registered for a binding, processKeyBinding is invoked on the component with a condition of WHEN_IN_FOCUSED_WINDOW. Similiar to the two previous steps, if a binding exists, the receiver is enabled and the action is enabled the action is notified and the event is consumed.
    9. Lastly, the KeyboardManager will give any JMenuBar's a chance to consume the event by invoking processKeyBinding on the JMenuBar's until one consumes the event.

    Avoiding Allocation

    There are a number of opportunities for sharing with this scheme. The InputMaps (with the exception of the ComponentInputMap) and ActionMap provided by the UI can be shared among instances of the same class. In order to share the InputMap, the bindings must not change based on the state of an instance. For example, the mnemonic of a component changes based on developer settings implying this InputMap can not be shared (this example isn't that good, the mnemonic is a binding of type WHEN_IN_FOCUSED_WINDOW, implying it is to be in the ComponentInputMap and can not be shared, but in looking at the classes I have not come across one that dynamicly changes bindings for WHEN_FOCUSED or WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).

    When you wouldn't share ActionMaps:

    Bindings in the defaults table

    The bindings (between KeyStroke and Action name) for a component will be registered in the defaults table. The bindings will be an Object array containing KeyStrokes as Strings and Action names (refer to the javadoc of getKeyStroke(String) for the format of the Strings in the binding array). JRadioButton has the following bindings:

        "RadioButton.focusInputMap",
           new UIDefaults.LazyInputMap(new Object[]
            {
                         "SPACE", "pressed",
                "released SPACE", "released"
            }),
    

    The first time the value "RadioButton.focusInputMap" is asked for from the UIManager LazyInputMap will create a InputMapUIResource from the Object array passed in.

    A InputMap entry can be used for InputMaps that will be registered under WHEN_FOCUSED (focusInputMap) and WHEN_ANCESTOR_OF_FOCUSED_COMPONENT (ancestorInputMap) but not for WHEN_IN_FOCUSED_WINDOW. Remember the WHEN_IN_FOCUSED_WINDOW InputMap is of type ComponentInputMap and can not be shared. It is perfectly fine to express the WHEN_IN_FOCUSED_WINDOW bindings in the defaults, but not the InputMap. The following shows the WHEN_IN_FOCUSED_WINDOW bindnigs for JDesktopPane:

        "Desktop.windowBindings", new Object[]
          {  "ctrl F9", "minimize", 
    	"ctrl F10", "maximize",
    	 "ctrl F4", "close",
    	 "ctrl F6", "navigate",
    	"ctrl TAB", "navigate"
          },
    

    BasicDesktopPaneUI creates a InputMap (actually, a ComponentInputMapUIResource) from the above by way of:

        Object[] bindings = (Object[])UIManager.get
    		                          ("Desktop.windowBindings");
    
        if (bindings != null) {
    	keymap = LookAndFeel.makeComponentInputMap(desktop, bindings);
        }
    

    Changing the plaf classes

    Almost all plaf classes have the protected method installKeyboardActions to register any keyboard actions. Instead of having these methods call registerKeyboardActions they will now install the necessary InputMaps and ActionMap. In modifying the plaf classes, I have added the following methods (all currently package private):

    A typical installKeyboardActions method now looks like:

        protected void installKeyboardActions() {
    	InputMap keyMap = getInputMap(JComponent.WHEN_FOCUSED);
    
    	SwingUtilities.replaceUIInputMap(list, JComponent.WHEN_FOCUSED, keyMap);
    
    	ActionMap map = getActionMap();
    
            SwingUtilities.replaceUIActionMap(list, map);
        }
    

    Notice that currently installKeyboardActions is only installing InputMaps for the bindings it expects. That is, the above only looks for a InputMap of type WHEN_FOCUSED, since currently that is the only binding registered. Similiarly, if a component did nothing in its installKeyboardActions method, it still does nothing.

    And the getInputMap() and getActionMap() methods look like:

        InputMap getInputMap(int condition) {
    	if (condition == JComponent.WHEN_FOCUSED) {
    	    return (InputMap)UIManager.get("List.focusInputMap");
    	}
    	return null;
        }
    
        ActionMap getActionMap() {
    	ActionMap map = (ActionMap)UIManager.get("List.actionMap");
    
    	if (map == null) {
    	    map = createActionMap();
    	    if (map != null) {
    		SwingUtilities.put("List.actionMap", map);
    	    }
    	}
    	return map;
        }
    
        ActionMap createActionMap() {
    	ActionMap map = new ActionMapUIResource();
            map.put("selectPreviousRow", new ....)
            // register all the Actions JList supports.
            return map;
        }
    

    And the uninstallKeyboardActions looks like:

        protected void uninstallKeyboardActions() {
    	SwingUtilities.replaceUIActionMap(list, null);
    	SwingUtilities.replaceUIInputMap(list, JComponent.WHEN_FOCUSED, null);
        }
    

    The text components

    The text components already have the notion of associating KeyStrokes with Actions. The Keymap (note the small m) class exists in the javax.swing.text package and associates a KeyStroke with an Action. We dot not plan on deprecating this, instead we will wrap Keymaps inside of a special ActionMap and InputMap. The plan is to modify the setKeymap method of JTextComponent to wrap the Keymap into a InputMap/ActionMap that is set as the parent of the InputMap/ActionMap provided by the component. This custom InputMap/ActionMap will NOT implement UIResource, so as the UI can provide keybindings, if it wishes. Confused? Take a JTextField, with a focusAccelerator, as an example. There will be the InputMap directly associated with the Comonent, call it cKM, there will be the InputMap that is wrapping the Keymap, calling it kmKM, and there will be the InputMap provided by the UI (call it uiKM). This will have the following structure:

        cKM -> kmKM -> uiKM
    

    If the Keymap is reset (via JTextComponent.setKeymap()), a new KeymapWrapper will be created, call it kmKM2 that will result in the following structure:

        cKM -> kmKM2 -> uiKM
    

    Setting the Keymap to null, will result in the usual InputMap structure:

        cKM -> uiKM
    

    KeymapWrapper and KeymapActionMap are private inner classes of JTextComponent. By wrapping Keymap like this, we do not have to have any special code in JTextComponent to deal with Keymaps, they are essentialy a specialized version of InputMap and ActionMap. Further, builders will not have to specially handle JTextComonent's to discover their bindings.

    A case study: JButton

    JButton has two WHEN_FOCUSED bindings, one for when the space bar is pressed and the second for when the space bar is released. BasicLookAndFeel will contain the following entry expressing this:

    	    "Button.focusInputMap", new UIDefaults.LazyInputMap(new Object[]
                  {
                             "SPACE", "pressed",
                    "released SPACE", "released"
                  }),
    

    The focusInputMap is installed in the installKeyboardActions method, via the following:

    
    	InputMap km = getInputMap(JComponent.WHEN_FOCUSED, c);
    
    	SwingUtilities.replaceUIInputMap(c, JComponent.WHEN_FOCUSED, km);
    

    Where getInputMap looks like:

        InputMap getInputMap(int condition, JComponent c) {
    	if (condition == JComponent.WHEN_FOCUSED) {
    	    ButtonUI ui = ((AbstractButton)c).getUI();
    	    if (ui != null && (ui instanceof BasicButtonUI)) {
    		return (InputMap)UIManager.get(((BasicButtonUI)ui).
    				       getPropertyPrefix() +"focusInputMap");
    	    }
    	}
    	return null;
        }
    

    JButton (actually, AbstractButton) allows the user to set a mnemonic. Mnemonics are registered using WHEN_IN_FOCUSED_WINDOW, implying ButtonUI needs to supply a ComponentInputMap. When the mnemonic changes (BasicButtonListener is notified via a PropertyChangeListener) it will update the ComponentInputMap. This code looks like:

        int m = b.getMnemonic();
        InputMap keyMap = getWindowInputMap(c);
        keyMap.clear();
        if(m != 0) {
            keyMap.put(KeyStroke.getKeyStroke(m, ActionEvent.ALT_MASK, false),
    		       "pressed");
    	keyMap.put(KeyStroke.getKeyStroke(m, ActionEvent.ALT_MASK, true),
    		       "released");
    	keyMap.put(KeyStroke.getKeyStroke(m, 0, true), "released");
        } 
    

    The last step is to supply the ActionMap containing the implementation for the "pressed" and "released" actions. These actions are conditionally enabled based on the state of the button, implying they can not be shared. BasicButtonUI will provide a new ActionMap for each JButton instance that is created at installUI time. This is created by the following code:

        ActionMap map = new ActionMapUIResource();
    
        map.put("pressed", new PressedAction((AbstractButton)c));
        map.put("released", new ReleasedAction((AbstractButton)c));
    

    So, JButton creates an ActionMap, and ComponentInputMap for each instance, and shares a InputMap between all JButton instances. Ideally we could share the ActionMap as well if isEnabled was passed a Component...

    Here is the complete set of changes to enable InputMaps/ActionMaps in BasicButtonListener:

        public void installKeyboardActions(JComponent c) {
    	AbstractButton b = (AbstractButton)c;	
    	// Update the mnemonic binding.
    	updateMnemonicBinding(b);
    
    	// Reset the ActionMap.
    	ActionMap map = getActionMap(b);
    
    	SwingUtilities.replaceUIActionMap(map, c);
    
    	InputMap km = getInputMap(JComponent.WHEN_FOCUSED, c);
    
    	SwingUtilities.replaceUIInputMap(c, JComponent.WHEN_FOCUSED, km);
        }
    
        public void uninstallKeyboardActions(JComponent c) {
    	if (createdWindowInputMap) {
    	    SwingUtilities.replaceUIInputMap(c, JComponent.
    					   WHEN_IN_FOCUSED_WINDOW, null);
    	    createdWindowInputMap = false;
    	}
    	SwingUtilities.replaceUIInputMap(c, JComponent.WHEN_FOCUSED, null);
    	SwingUtilities.replaceUIActionMap(c, null);
        }
    
        ActionMap getActionMap(AbstractButton b) {
    	return createActionMap(b);
        }
    
        InputMap getInputMap(int condition, JComponent c) {
    	if (condition == JComponent.WHEN_FOCUSED) {
    	    ButtonUI ui = ((AbstractButton)c).getUI();
    	    if (ui != null && (ui instanceof BasicButtonUI)) {
    		return (InputMap)UIManager.get(((BasicButtonUI)ui).
    				       getPropertyPrefix() +"focusInputMap");
    	    }
    	}
    	return null;
        }
    
        ActionMap createActionMap(AbstractButton c) {
    	ActionMap retValue = new javax.swing.plaf.ActionMapUIResource();
    
    	retValue.put("pressed", new PressedAction((AbstractButton)c));
    	retValue.put("released", new ReleasedAction((AbstractButton)c));
    	return retValue;
        }
    
        void updateMnemonicBinding(AbstractButton b) {
    	int m = b.getMnemonic();
    	if(m != 0) {
    	    InputMap map;
    	    if (!createdWindowInputMap) {
    		map = new ComponentInputMapUIResource(b);
    		SwingUtilities.replaceUIInputMap(b,
    			       JComponent.WHEN_IN_FOCUSED_WINDOW, map);
    		createdWindowInputMap = true;
    	    }
    	    else {
    		map = SwingUtilities.getUIInputMap(JComponent.
    						 WHEN_IN_FOCUSED_WINDOW, b);
    	    }
    	    if (map != null) {
    		map.clear();
    		map.put(KeyStroke.getKeyStroke(m, ActionEvent.ALT_MASK, false),
    			"pressed");
    		map.put(KeyStroke.getKeyStroke(m, ActionEvent.ALT_MASK, true),
    		       "released");
    		map.put(KeyStroke.getKeyStroke(m, 0, true), "released");
    	    }
    	} 
    	else if (createdWindowInputMap) {
    	    InputMap map = SwingUtilities.getUIInputMap(JComponent.
    					     WHEN_IN_FOCUSED_WINDOW, b);
    	    if (map != null) {
    		map.clear();
    	    }
    	}
        }
    

    Notice the trickery with getPropertyPrefix(), this is used as BasicButtonListener is used for a number of classes, not just JButton. Also notice the updateMnemonicBinding, this is called when the mnemonic property changes and will update the WHEN_IN_FOCUSED_WINDOW InputMap as a result of the mnemonic changing.

    Notes

    A couple of things should be noted about moving to InputMap/ActionMap:


    Hans Muller
    and
    Scott Violet
    Last modified: Fri Jun 4 13:00:54 PDT 1999