It is common to have multiple ways to activate the same command. The user can choose a certain function through a menu, a keystroke, or a button on a toolbar. This is easy to achieve in the AWT event model: link all events to the same listener. For example, suppose blueAction is an action listener whose actionPerformed method changes the background color to blue. You can attach the same object as a listener to several event sources:
Then the color change command is handled in a uniform way, no matter whether it was caused by a button click, a menu selection, or a key press. The Swing package provides a very useful mechanism to encapsulate commands and to attach them to multiple event sources: the Action interface. An action is an object that encapsulates
The Action interface has the following methods: void actionPerformed(ActionEvent event) void setEnabled(boolean b) boolean isEnabled() void putValue(String key, Object value) Object getValue(String key) void addPropertyChangeListener(PropertyChangeListener listener) void removePropertyChangeListener(PropertyChangeListener listener) The first method is the familiar method in the ActionListener interface: in fact, the Action interface extends the ActionListener interface. Therefore, you can use an Action object whenever an ActionListener object is expected. The next two methods let you enable or disable the action and check whether the action is currently enabled. When an action is attached to a menu or toolbar and the action is disabled, then the option is grayed out. The putValue and getValue methods let you store and retrieve arbitrary name/value pairs in the action object. A couple of important predefined strings, namely, Action.NAME and Action.SMALL_ICON, store action names and icons into an action object: action.putValue(Action.NAME, "Blue"); action.putValue(Action.SMALL_ICON, new ImageIcon("blue-ball.gif")); Table 8-3 shows all predefined action table names.
If the action object is added to a menu or toolbar, then the name and icon are automatically retrieved and displayed in the menu item or toolbar button. The SHORT_DESCRIPTION value turns into a tooltip. The final two methods of the Action interface allow other objects, in particular menus or toolbars that trigger the action, to be notified when the properties of the action object change. For example, if a menu is added as a property change listener of an action object and the action object is subsequently disabled, then the menu is called and can gray out the action name. Property change listeners are a general construct that is a part of the "JavaBeans" component model. You can find out more about beans and their properties in Volume 2. Note that Action is an interface, not a class. Any class implementing this interface must implement the seven methods we just discussed. Fortunately, a friendly soul has provided a class AbstractAction that implements all methods except for actionPerformed. That class takes care of storing all name/value pairs and managing the property change listeners. You simply extend AbstractAction and supply an actionPerformed method. Let's build an action object that can execute color change commands. We store the name of the command, an icon, and the desired color. We store the color in the table of name/value pairs that the AbstractAction class provides. Here is the code for the ColorAction class. The constructor sets the name/value pairs, and the actionPerformed method carries out the color change action. public class ColorAction extends AbstractAction { public ColorAction(String name, Icon icon, Color c) { putValue(Action.NAME, name); putValue(Action.SMALL_ICON, icon); putValue("color", c); putValue(Action.SHORT_DESCRIPTION, "Set panel color to " + name.toLowerCase()); } public void actionPerformed(ActionEvent event) { Color c = (Color) getValue("color"); setBackground(c); } } Our test program creates three objects of this class, such as Action blueAction = new ColorAction("Blue", new ImageIcon("blue-ball.gif"), Color.BLUE); Next, let's associate this action with a button. That is easy because we can use a JButton constructor that takes an Action object. JButton blueButton = new JButton(blueAction); That constructor reads the name and icon from the action, sets the short description as the tooltip, and sets the action as the listener. You can see the icons and a tooltip in Figure 8-9. Figure 8-9. Buttons display the icons from the action objectsAs we demonstrate in the next chapter, it is just as easy to add the same action to a menu. Finally, we want to add the action objects to keystrokes so that the actions are carried out when the user types keyboard commands. Now we run into a technical complexity. Keystrokes are delivered to the component that has focus. Our sample application is made up of several components, namely, three buttons inside a panel. Therefore, at any time, any one of the three buttons may have focus. Each of the buttons would need to handle key events and listen to the CTRL+Y, CTRL+B, and CTRL+R keys. This is a common problem, and the Swing designers came up with a convenient solution for solving it. NOTE
To associate actions with keystrokes, you first need to generate objects of the KeyStroke class. This is a convenience class that encapsulates the description of a key. To generate a KeyStroke object, you don't call a constructor but instead use the static getKeyStroke method of the KeyStroke class. You specify the virtual key code and the flags (such as SHIFT and CONTROL key combinations): KeyStroke ctrlBKey = KeyStroke.getKeyStroke(KeyEvent.VK_B, InputEvent.CTRL_MASK); Alternatively, you can describe the keystroke as a string: KeyStroke ctrlBKey = KeyStroke.getKeyStroke("ctrl B"); Every JComponent has three input maps, each mapping KeyStroke objects to associated actions. The three input maps correspond to three different conditions (see Table 8-4).
Keystroke processing checks these maps in the following order:
You obtain an input map from the component with the getInputMap method, for example: InputMap imap = panel.getInputMap(JComponent.WHEN_FOCUSED); The WHEN_FOCUSED condition means that this map is consulted when the current component has the keyboard focus. In our situation, that isn't the map we want. One of the buttons, not the panel, has the input focus. Either of the other two map choices works fine for inserting the color change keystrokes. We use WHEN_ANCESTOR_OF_FOCUSED_COMPONENT in our example program. The InputMap doesn't directly map KeyStroke objects to Action objects. Instead, it maps to arbitrary objects, and a second map, implemented by the ActionMap class, maps objects to actions. That makes it easier to share the same actions among keystrokes that come from different input maps. Thus, each component has three input maps and one action map. To tie them together, you need to come up with names for the actions. Here is how you can tie a key to an action: imap.put(KeyStroke.getKeyStroke("ctrl Y"), "panel.yellow"); ActionMap amap = panel.getActionMap(); amap.put("panel.yellow", yellowAction); It is customary to use the string "none" for a do-nothing action. That makes it easy to deactivate a key: imap.put(KeyStroke.getKeyStroke("ctrl C"), "none"); CAUTION
To summarize, here is what you do to carry out the same action in response to a button, a menu item, or a keystroke:
Example 8-5 shows the complete code of the program that maps both buttons and keystrokes to action objects. Try it out clicking either the buttons or pressing CTRL+Y, CTRL+B, or CTRL+R changes the panel color. Example 8-5. ActionTest.java1. import java.awt.*; 2. import java.awt.event.*; 3. import javax.swing.*; 4. 5. public class ActionTest 6. { 7. public static void main(String[] args) 8. { 9. ActionFrame frame = new ActionFrame(); 10. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 11. frame.setVisible(true); 12. } 13. } 14. 15. /** 16. A frame with a panel that demonstrates color change actions. 17. */ 18. class ActionFrame extends JFrame 19. { 20. public ActionFrame() 21. { 22. setTitle("ActionTest"); 23. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); 24. 25. // add panel to frame 26. 27. ActionPanel panel = new ActionPanel(); 28. add(panel); 29. } 30. 31. public static final int DEFAULT_WIDTH = 300; 32. public static final int DEFAULT_HEIGHT = 200; 33. } 34. 35. /** 36. A panel with buttons and keyboard shortcuts to change 37. the background color. 38. */ 39. class ActionPanel extends JPanel 40. { 41. public ActionPanel() 42. { 43. // define actions 44. 45. Action yellowAction = new ColorAction("Yellow", 46. new ImageIcon("yellow-ball.gif"), 47. Color.YELLOW); 48. Action blueAction = new ColorAction("Blue", 49. new ImageIcon("blue-ball.gif"), 50. Color.BLUE); 51. Action redAction = new ColorAction("Red", 52. new ImageIcon("red-ball.gif"), 53. Color.RED); 54. 55. // add buttons for these actions 56. 57. add(new JButton(yellowAction)); 58. add(new JButton(blueAction)); 59. add(new JButton(redAction)); 60. 61. // associate the Y, B, and R keys with names 62. 63. InputMap imap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 64. 65. imap.put(KeyStroke.getKeyStroke("ctrl Y"), "panel.yellow"); 66. imap.put(KeyStroke.getKeyStroke("ctrl B"), "panel.blue"); 67. imap.put(KeyStroke.getKeyStroke("ctrl R"), "panel.red"); 68. 69. // associate the names with actions 70. 71. ActionMap amap = getActionMap(); 72. amap.put("panel.yellow", yellowAction); 73. amap.put("panel.blue", blueAction); 74. amap.put("panel.red", redAction); 75. } 76. 77. public class ColorAction extends AbstractAction 78. { 79. /** 80. Constructs a color action. 81. @param name the name to show on the button 82. @param icon the icon to display on the button 83. @param c the background color 84. */ 85. public ColorAction(String name, Icon icon, Color c) 86. { 87. putValue(Action.NAME, name); 88. putValue(Action.SMALL_ICON, icon); 89. putValue(Action.SHORT_DESCRIPTION, "Set panel color to " + name.toLowerCase()); 90. putValue("color", c); 91. } 92. 93. public void actionPerformed(ActionEvent event) 94. { 95. Color c = (Color) getValue("color"); 96. setBackground(c); 97. } 98. } 99. } javax.swing.Action 1.2
javax.swing.JMenu 1.2
javax.swing.KeyStroke 1.2
javax.swing.JComponent 1.2
|