ORIGINAL DRAFT

With Java 1.3 came the introduction of a unified mechanism for dealing with key stroke mappings and action assignments. The new Java APIs includes the use of an InputMap to map key strokes to named actions and an ActionMap to map action named to the actual Action objects. This approach allows you to define actions in a loosely coupled manner, assigning key strokes to the action names instead of directly to the action objects. Both the InputMap and ActionMap classes support inheritance from parent objects so that key assignments can be propagated up the component hierarchy.

This month, we’ll be developing a component that lets you remove or assign key strokes to a set of named actions. The JKeyMap component provides a view of existing mappings, in table form, and an editable text field that manages KeyStroke objects. We’ll overcome a few minor problems with the KeyStroke toString method in order to support a more effective presentation, and we’ll assemble these components in a JPanel so that you can use it in any interface window.

Figure 1: JKeyMap allows you to edit InputMap assignments.
The interface provides a KeyStrokeField to do the editing so that key assignments can be easily managed.

Figure 1: JKeyMap allows you to edit InputMap assignments. The interface provides a KeyStrokeField to do the editing so that key assignments can be easily managed.

JKeyMap is designed to support InputMap management. It expects you to use the getInputMap method available in any JComponent, call the setModelInputMap method on the JKeyMap object, do your editing, and then call the JKeyMap getModelInputMap method and set the values on the JComponent by using it’s setInputMap method. Figure 1 shows JKeyMap in action. Typing any key combination while the text field has focus will display appropriate text in the field view. Pressing the Assign button will assign that key combination to the currently selected action item in the table view. Pressing the Remove button clears the currently assigned action.

Figure 2: JKeyMap classes. The KeyMapTable and
KeyStrokeFields provide the interface elements that let us view and edit KeyStroke objects and their
associations with named Actions.

Figure 2: JKeyMap classes. The KeyMapTable and KeyStrokeFields provide the interface elements that let us view and edit KeyStroke objects and their associations with named Actions.

Let’s take a look at some of the code. Figure 2 shows the relationship between the classes we’ll develop. The JKeyMapTest class is merely a test harness and you can take a closer look at it when you download the code from www.java-pro.com. We’ll take a closer look at JKeyMap, KeyMapTableModel and KeyStrokeFields, which are the more important classes.

The KeyMapTable classes merely extends JTable and sets the KeyMapRenderer and a few other parameters in a single place. The KeyMapRenderer class extends the Swing DefaultTableCellRenderer and merely adds a little formatting for the KeyStroke objects. Both are less that a dozen lines of code and are simple enough to understand by just glancing at the code.

Listing 1 shows the KeyMapTableModel class. We inherit from the DefaultTableModel in order to minimize the work we need to do. The constructor calls the addColumn method to name two columns. This has the added effect of creating the columns internally and is better than overriding the getColumnName method. The two methods we need to implement allow us to map between an InputMap object and the TableModel.

The setModelInputMap method takes an InputMap and produces a sorted list of action name/key stroke pairs by adding each InputMap entry to a SortedMap instance. We then walk through the sorted data and add each entry to the model after removing any existing entries. The getModelInputMap creates a new InputMap and walks the model to copy the action name/key stroke pairs into the InputMap before returning it.

Listing 2 shows the code for the KeyStrokeField. Our objective is to look and feel exactly like any other text field, with one minor exception. We don’t want to allow the JTextField we inherit from to interpret our key strokes. Instead, we want the key strokes to be expanded into text when we type them. To override the first behavior, we set the InputMap parent for JTextField to null and install a key listener so that we can handle the input directly. On a key press, we set the keyStroke instance variable and clear the text field to avoid partial information from modifier keys, such as the control, shift and alt keys.

When the key is released, we show the key stroke in text form. Unfortunately, the KeyStroke object toString method falls short of what we need, so I’ve developed a formatKeyStroke method to do the formatting. This method is static so it can be used elsewhere, such as in our KeyMapCellRenderer. The KeyEvent class has a couple of static formatting methods, getKeyText and getModifierText which are helpful. The formatKeyStroke takes care of additional variants such as a null KeyStroke and matching key code and modifier values.

Listing 3 shows the code for the JKeyMap class, which ties it all together. Most of the code in the constructor is concerned with laying out the components for visual presentation and assigning the table, field and buttons to instance variables for later reference. We implement the ActionListerner and ListSelectionListener interfaces and register to receive these events from constituent components. The actionPerformed method respond to the Assign and Remove buttons, while the valueChanged allows is to track table selection and keep the KeyStroke displayed in the field in synch. We add two methods that map the getModelInputMap and setModelInputMap methods onto the table model.

With JKeyMap, you now have the ability to edit InputMap key stroke assignments in your interfaces. This is a powerful tool for end users, especially when applied to complex components like text editors. In full-function interfaces, allowing the user to reassign key strokes introduces considerable flexibility and JKeyMap minimizes the work involved in providing that kind of functionality. The new InputMap and ActionMap paradigm introduced in Java 1.3 provides new possibilities for user interface designers. I hope JKeyMap can help you leverage these to your advantage.

Listing 1

import java.awt.*;
import java.util.*;
import javax.swing.*;
import javax.swing.table.*;

public class KeyMapTableModel extends DefaultTableModel
{
  public KeyMapTableModel()
  {
    addColumn("Action");
    addColumn("Key Assignment");
  }
  
  public void setModelInputMap(InputMap inputMap)
  {
    KeyStroke[] keys = inputMap.allKeys();
    SortedMap sortedMap = new TreeMap();
    for (int i = 0; i < keys.length; i++)
    {
      sortedMap.put(inputMap.get(keys[i]).toString(), keys[i]);
    }
    int count = getRowCount();
    for (int i = count - 1; i >= 0; i--)
    {
      removeRow(i);
    }
    Iterator iterator = sortedMap.entrySet().iterator();
    while (iterator.hasNext())
    {
      Map.Entry entry = (Map.Entry)iterator.next();
      Object[] col = {entry.getKey(), entry.getValue()};
      addRow(col);
    }
  }
  
  public InputMap getModelInputMap()
  {
    InputMap inputMap = new InputMap();
    for (int i = 0; i < getRowCount(); i++)
    {
      Object stroke = getValueAt(i, 1);
      if (stroke instanceof KeyStroke)
      {
        inputMap.put((KeyStroke)stroke,
          (String)getValueAt(i, 0));
      }
    }
    return inputMap;
  }
}

Listing 2

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class KeyStrokeField extends JTextField
  implements KeyListener
{
  protected KeyStroke keyStroke;
  
  public KeyStrokeField()
  {
    // No cut/paste inheritance.
    getInputMap().setParent(null);
    addKeyListener(this);
  }

  public static String formatKeyStroke(KeyStroke keyStroke)
  {
    if (keyStroke == null) return "";
    int keyCode = keyStroke.getKeyCode();
    int modifiers = keyStroke.getModifiers();
    if (keyCode == KeyEvent.VK_SHIFT) keyCode = 0;
    if (keyCode == KeyEvent.VK_CONTROL) keyCode = 0;
    if (keyCode == KeyEvent.VK_ALT) keyCode = 0;
    String key = keyCode == 0 ? "" : KeyEvent.getKeyText(keyCode);
    String mods = KeyEvent.getKeyModifiersText(modifiers);
    if (key.equalsIgnoreCase(mods)) mods = "";
    if (keyCode == 0) mods = "";
    if (mods.length() > 0) mods += "+";
    return mods + key;
  }
  
  public KeyStroke getKeyStroke()
  {
    return keyStroke;
  }
  
  public void setKeyStroke(KeyStroke keyStroke)
  {
    this.keyStroke = keyStroke;
    setText(formatKeyStroke(keyStroke));
  }

  public void keyTyped(KeyEvent event) {}

  public void keyPressed(KeyEvent event)
  {
    keyStroke = KeyStroke.getKeyStrokeForEvent(event);
    setText("");
  }
  
  public void keyReleased(KeyEvent event)
  {
    setText(formatKeyStroke(keyStroke));
  }
}

Listing 3

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;

public class JKeyMap extends JPanel
  implements ActionListener, ListSelectionListener
{
  protected KeyMapTable keyMapTable;
  protected KeyMapTableModel model;
  protected KeyStrokeField keyStrokeField;
  protected JButton assign, remove;

  public JKeyMap()
  {
    model = new KeyMapTableModel();
    //model.setModelInputMap(component.getInputMap());
    
    JPanel table = new JPanel(new GridLayout());
    table.add(new JScrollPane(
      keyMapTable = new KeyMapTable(model)));
    keyMapTable.getSelectionModel().addListSelectionListener(this); 
    table.setPreferredSize(new Dimension(400, 150));

    JPanel north = new JPanel(new GridLayout(2, 1, 4, 4));
    north.add(assign = new JButton("Assign"));
    north.add(remove = new JButton("Remove"));
    assign.setDefaultCapable(false);
    remove.setDefaultCapable(false);

    JPanel buttons = new JPanel(new BorderLayout());
    buttons.add(BorderLayout.NORTH, north);

    JPanel field = new JPanel(new BorderLayout(4, 4));
    field.add(BorderLayout.WEST, new JLabel("New key assignment:"));
    field.add(BorderLayout.CENTER, 
      keyStrokeField = new KeyStrokeField());
    
    JPanel main = new JPanel(new BorderLayout(8, 8));
    main.add(BorderLayout.CENTER, table);
    main.add(BorderLayout.SOUTH, field);
    
    setLayout(new BorderLayout(8, 8));
    setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
    add(BorderLayout.CENTER, main);
    add(BorderLayout.EAST, buttons);
    
    assign.addActionListener(this);
    remove.addActionListener(this);
  }
  
  public void setModelInputMap(InputMap inputMap)
  {
    model.setModelInputMap(inputMap);
  }
  
  public InputMap getModelInputMap()
  {
    return model.getModelInputMap();
  }
  
  public void actionPerformed(ActionEvent event)
  {
    Object source = event.getSource();
    int row = keyMapTable.getSelectedRow();
    if (source == assign)
    {
      KeyStroke stroke = keyStrokeField.getKeyStroke();
      model.setValueAt(stroke, row, 1);
    }
    if (source == remove)
    {
      model.setValueAt("", row, 1);
      keyStrokeField.setKeyStroke(null);
    }
  }

  public void valueChanged(ListSelectionEvent event)
  {
    int row = keyMapTable.getSelectedRow();
    Object value = model.getValueAt(row, 1);
    if (value instanceof KeyStroke)
    {
      keyStrokeField.setKeyStroke((KeyStroke)value);
    }
    else
    {
      keyStrokeField.setKeyStroke(null);
    }
  }
}