CPSC 225, Spring 2011, April 18:
Keyboarding with InputMap and ActionMap

The purpose of this tutorial is to demonstrate the use of the InputMap and ActionMap of a component to do keyboard processing.

Keyboard processing with KeyListeners is covered in Section 6.5 of the textbook. For some applications, that approach is not ideal, since you have to worry about managing the keyboard focus. That requires using a FocusListener so that you can know when you have the focus. And it often requires using a MouseListener simply to request the focus when the user clicks. The InputMap/ActionMap approach avoids the use of a focus listener and mouse listener.

This tutorial is based on the example SubKillerPanel.java from Subsection 6.5.4. You can find the file in /classes/s11/cs225/tutorials. Add it to an Eclipse project. (Try running SubKillerPanel before you make any changes.) A version of the program after completing this tutorial can be found in the executable jar file SubKillerComplete.jar.


Using InputMap and ActionMap

Every component has an ActionMap and several InputMaps. These work together to handle user keyboard input. An InputMap maps a keystroke to a string, and an ActionMap maps a string to an action. The keystoke represents user keyboard input; the action is what you want to happen when the keystroke occurs. The string just connects the two; you can use any string you want. (It's unfortunate from our point of view that there are two maps: It would seem more natural to map a keystroke directly to an action. However, we'll see later that there is a reason for this.)

You will be working in a JPanel, and you want that panel's input and action maps. Add the following code to the constructor of the SubKillerPanel class:

      InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
      ActionMap actionMap = getActionMap();

The parameter in the first line means that it will handle any keystrokes that occur when the window is active. There is no need for the panel to have the focus.

As a first example, you will make the program end when the user presses the ESCAPE key. To do that, simple add this code to the constructor:

      inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0), "quit" );
      
      actionMap.put("quit", new AbstractAction() {
          public void actionPerformed(ActionEvent e) {
              System.exit(0);
          }
      });

In the first line, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0) is a keystroke that represents the act of pressing the escape key while holding down no modifier keys. (The "0" indicates "no modifiers".) This keystroke is mapped to the string "quit" (which could have been anything at all). The second line maps the string "quit" to an AbstractAction, which represents an action to be taken by the program. The net result is that when the user presses ESCAPE, the code in the action's actionPerformed() method will be executed. In this case, that code ends the program.


No More Focus or Mouse Listeners!

Next, you can move the handler for the down-arrow key from the key listener to the input/action maps. The key listener in the program contains this (somewhat redundant) code:

            else if (code == KeyEvent.VK_DOWN) {
                  // Start the bomb falling, if it is not already falling.
               if ( bomb.isFalling == false )
                  bomb.isFalling = true;
            }

To move this functionality, you need to map the keystroke KeyEvent.VK_DOWN to some string in the input map, and you need to map that same string to an action in the action map. The code that says what happens when the user presses the down arrow goes in the actionPerformed method of the action:

          inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN,0), "downPressed");
          
          actionMap.put("downPressed", new AbstractAction() {
              public void actionPerformed(ActionEvent e) {
                  bomb.isFalling = true;
              }
          });

In fact, the goal is to get rid of the key listeners and the associated focus and mouse listeners entirely. So you should delete them all from the SubKillerPanel constructor.

In this program a timer drives the animation. Now, since a mouse click won't be used to start the game, you should start the timer in the constructor. Add the line

          timer.start();

You still have to make the left and right arrow keys work to move the boat from left to right. While we are making that change, let's improve the interaction a bit. In the original game, the left/right arrow keys move the boat by 15 pixels. When the arrow key is held down for a while, it "autorepeats," which keeps moving the boat. However, a much smoother interaction can be achieved by using the following strategy: Give the boat a "speed", which is usually zero, and add that speed to the boat's position when updating for the next frame. When the left arrow key is pressed, set the speed to, say, -5, and when the right arrow key is pressed, set the speed to 5. When either arrow key is released, set the speed back to zero. It's possible to implement this strategy with a KeyListener, which has both a keyPressed and keyReleased method, but let's do it with the input/action maps.

First of all, find the nested class "private class Boat" and add an instance variable speed of type int. In that class, you should also add the following line to the beginning of the updateForNextFrame method:

           centerX += speed;

To make a keystroke that represents releasing the left arrow key, you can use KeyStroke.getKeyStroke(KeyEvent.VK_LEFT,0,true). The last parameter says that the keystroke represents a key release rather than a key press. For a key press, you can use either KeyStroke.getKeyStroke(KeyEvent.VK_LEFT,0,false) or simply. KeyStroke.getKeyStroke(KeyEvent.VK_LEFT,0). So, to react to all these key strokes, you can add the following associations to the input map:

	   inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT,0,false), "leftPressed");
	   inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT,0,false), "rightPressed");
	   inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT,0,true), "release");
	   inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT,0,true), "release");

Note that both key releases are mapped to the same string, and they will trigger the same action. (The ability to do things like that is one reason to have separate input and action maps.) In the action map, the string "release" should be mapped to an action that sets boat.speed = 0:

      actionMap.put("release", new AbstractAction() {
    	  public void actionPerformed(ActionEvent e) {
    		  boat.speed = 0;
    	  }
      });

Add this to the constructor. Also add actions to set boat.speed to -5 or 5 when the left or right arrow key is pressed.

The game at this point should be playable. But you might want to make a few more changes. For example, make pressing the space bar pause and restart the game. (You can tell whether the game is currently paused by testing whether timer.isRunning(), and you can pause and restart the game using timer.stop() and timer.start().) You might also want to change the paintComponent method. Currently, it checks the value of hasFocus() to change the appearance of the game depending on whether or not the panel has the input focus. In the new version, the input focus is not being used, so this doesn't make sense.