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.
- Unify the existing keybinding facilities.
- Disconnect bindings from UI specific actions.
- Enable keymap sharing.
- Make tables of bindings easier to read.
- Expose the complete list of actions a component supports. Developers
should be able to extend this list easily.
- Self documenting.
- 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:
-
WHEN_FOCUSED
Valid when the component itself has the focus. Most bindings are
defined with this trigger condition.
-
WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
Valid if one of the components ancestors has the focus. Used by
containers to create default keybindings for their children. For
example BasicScrollPaneUI creates default bindings for page up/down
for it's view descendant this way.
-
WHEN_IN_FOCUSED_WINDOW
Valid if the components Window ancestor has a descendant with
the focus. Used by modal components to register keyboard navigation
bindings that allow one to drive the component without explicitly
giving it the focus. For example, setting a buttons mnemonic causes
a this kind of binding to be created.
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:
-
The registerComponentAction "condition"
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.
-
ActionEvent.getActionCommand() differences
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.
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()
|
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 hierarchy 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 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:
-
If the Action implements the
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.
-
If the Action is a non-static inner class. Potentially all of
our Actions should be static inner classes,
but this would be a backward imcompatible change that would
probably not be allowed.
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 bindings 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):
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
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:
- The actions in the ActionMap are not always useful attached
to another component. Shared actions usually use the source of
the ActionEvent to determine the appropriate component, implying
if the source is not the expected Component a ClassCastException
is likely to be thrown.
- In an attempt to be consistant, some of the names of the actions
have changed.
- Keybinding actions will not be invoked if the component isn't
enabled. So, your actions don't have to override isEnabled with
a return value based on the enabled state of the component.
- Previously the modifers passed in the ActionEvent to ActionListeners
registered via registerKeyboardActions was 0, the modifiers will
now reflect the modifiers of the KeyEvent.
- Previously, the Keymap associated with a JTextComponent was
processed before any registered bindings, this is no longer the
case.
- Although difficult to get in this situation, it was possible
for the text actions to be invoked when the text component was
disabled, this is no longer possible.
- Setting the InputMap/ActionMap to null is allowed, and removes
any bindings installed by the UI. The sequence setInputMap(null),
setInputMap(new InputMap()) will not reinstall the UI InputMap.
|