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.
This is a list of all of the requirements we've tried to satisfy, roughly in priority order.
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.
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:
WHEN_FOCUSED
WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
WHEN_IN_FOCUSED_WINDOW
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.]
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.
There are two serious incompatabilities between the JComponent registerKeyboardAction machinery and the Keymap based system in the text package:
The text package doesn't explicitly allow for creating bindings that
apply when a descendant has the focus or when the components
ancestor window has the focus. The JComponent
registerComponentAction()
use a condition of
WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
or
WHEN_IN_FOCUSED_WINDOW
for these cases. Most of the bindings
in Swing are defined with condition == WHEN_FOCUSED
.
The JComponent registerKeyboardAction
methods allow one
to specify what the ActionEvent command will be. Most of the non
text Swing components use an informational name for the command.
Dependencies on these names have started to creep in, e.g. in the
accessibility code. The text components rely on the ActionEvents
"actionCommand" property being string version of the KeyEvents
keyChar property. Keymaps don't allow one to specify a overriding
value to use for actionCommand. We can work around this
incompatibility by allowing Action objects to provide the command
string, and then using the keyChar string when the Actions command
string is null. See below.
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.
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 *Object
s * (called keys orAction
names) * toAction
s. * AnActionMap
is usually used with anInputMap
* to locate a particular action * when a key is pressed. As withInputMap
, * anActionMap
can have a parent * that is searched for keys not defined in theActionMap
. * * @version 1.4 06/03/99 * @author Scott Violet * @since 1.3 */ public class ActionMap implements Serializable { /** * Creates anActionMap
with no parent and no mappings. */ public ActionMap() /** * Sets thisActionMap
's parent. * * @param map theActionMap
that is the parent of this one */ public void setParent(ActionMap map) /** * Returns thisActionMap
's parent. * * @return theActionMap
that is the parent of this one, * or null if thisActionMap
has no parent */ public ActionMap getParent() /** * Adds a binding forkey
toaction
. * Ifaction
is null, this removes the current binding * forkey
. *In most instances,
key
will be *action.getValue(NAME)
. */ public void put(Object key, Action action) /** * Returns the binding forkey
, messaging the * parentActionMap
if the binding is not locally defined. */ public Action get(Object key) /** * Removes the binding forkey
from thisActionMap
. */ public void remove(Object key) /** * Removes all the mappings from thisActionMap
. */ public void clear() /** * Returns theAction
names that are bound in thisActionMap
. */ public Object[] keys() /** * Returns the number ofKeyStroke
bindings. */ public int size() /** * Returns an array of the keys defined in thisActionMap
and * its parent. This method differs fromkeys()
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):
- 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!
- 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.
- Any registered KeyListeners are notified by invoking super.processKeyEvent.
- processComponentKeyEvent is invoked to allow any customizations in a subclasses to happen.
- 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).
- 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.
- 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.
- 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.
- 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:
isEnabled()
method
to conditionaly return a value based on an instance.
actionPerformed
is passed the component the
action is performed on, which provides a way to determine the
instance allowing for sharing, on the other hand,
isEnabled()
is not passed the source. Putting the
isEnabled()
check in the
actionPerformed()
method does not provide the
same behavior, the event will be consumed and not allow
another component to see the event. Consider a disabled menu
item and button with the same mnemonic, if the menu item Action
where to be shared isEnabled would always return true. This
would give the effect of the menu item consuming the key event
even though the button has the same binding, and is enabled.
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); }
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):
InputMap getInputMap(int)
which is to return the
InputMap for the sepcified condition. If the component does not
share the InputMap for the specified condition this method will
call createInputMap()
each time it is invoked.
InputMap createInputMap(int)
to create a
InputMap. This is currently only invoked for InputMaps of type
WHEN_IN_FOCUSED_WINDOW, all others are stored in the defaults
table so that the component UI never needs to instantiate
one. This method only exists for components that create a
InputMap per instance (most don't).
ActionMap getActionMap()
which returns the
appropriate ActionMap. If the ActionMap is not shared, this
method will call createActionMap
to create the
ActionMap. If the ActionMap is shared the ActionMap is stored
in the defaults table under the name XXX.actionMap. So, the
first time getActionMap
is invoked there would
be no entry, and createActionMap
would be
invoked, the return value would then be placed in the
defaults table.
createActionMap
to instantiate the ActionMap
with the appropriate Actions.
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 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.
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.
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