Main Page

Previous Next

Using Actions

One difficulty with the code that we have added to support the menus is that it is very menu specific. What I mean by this is that if we are going to do a proper job on the Sketcher application, we will undoubtedly want it to have a toolbar. The toolbar will surely have a whole bunch of buttons that perform exactly the same actions as the menu items we have just implemented, so we will be in the business of doing the same thing over again in the toolbar context. Of course, the only reason I brought it up, as I'm sure you anticipated, is that there is another way of working with menus, and that is to use an action object.

An action object is a bit of a strange beast, and it can be quite hard to understand at first so we will take it slowly. First of all let's look at what we mean by an 'action' here, as it is a precise term in this context. An action is an object of any class that implements the Action interface. This interface declares methods that operate on an action object, for example storing properties relating to the action, enabling it and disabling it. The Action interface happens to extend the ActionListener interface so an action object is a listener as well as an action. Now that we know an Action object can get and set properties, and is also a listener, how does that help us in implementing the Sketcher GUI?

The answer is in the last capability of an Action object. Some Swing components, such as those of type JMenu and JToolBar, have an add() method that accepts an argument of type Action. When you add an Action object to these using the add() method, the method creates a component from the Action object that is automatically of the right type. If you add an Action object to a JMenu object, a JMenuItem will be created and returned by the add() method. On the other hand, when you add exactly the same Action object to a JToolBar object, an object of type JButton will be created and returned. This means that you can add the very same Action object to both a menu and a toolbar, and since the Action object is its own listener you automatically get both supporting the same action. Clever, eh?

First, we should look at the Action interface.

The Action Interface

In general, properties are items of information that relate to a particular object and are stored as part of the object. Properties are often stored in a map, where a key identifies a particular property, and the value corresponding to that property can be stored in association with the key. The Properties class that is defined in the java.util package does exactly that. The Action interface has provision for storing seven basic standard properties that relate to an Action object:

  • A name – a String object that is used as the label for a menu item or a toolbar button.

  • A small icon – an Icon object to be displayed on a toolbar button.

  • A short description of the action – a String object to be used as a tooltip.

  • An accelerator key for the action – defined by a KeyStroke object.

  • A long description of the action —a String object that is intended to be used as context sensitive help.

  • A mnemonic key for the action – this is a key code of type int.

  • An action command key – defined by an entry in a KeyMap object associated with a component.

Just so you are aware of them I have included the complete set here, but we will concentrate on just using the first three. We haven't met Icon objects before, but we will get to them a little later in this chapter.

You are not obliged to provide for all of these properties in your action classes, but the interface provides the framework for it. These properties are stored internally in a map collection in your action class, so the Action interface defines constants that you use as keys for each of the standard properties. These constants are all of type String, and the ones we are interested in are NAME, SMALL_ICON, and SHORT_DESCRIPTION. The others are ACCELERATOR_KEY, LONG_DESCRIPTION, MNEMONIC_KEY, and ACTION_COMMAND_KEY. There is another constant of type String defined in the interface with the name DEFAULT. This is for you to use to store a default property for the action.

The Action interface also declares the following methods:

Method

Description

void putValue(String key, Object value)

Stores the value with the key key in the map supported by the action class. To store the name of an action within a class method you might write:

putValue(NAME, theName);

This uses the standard key, NAME to store the object theName.

Object getValue(String key)

This retrieves the object from the map corresponding to the key key. To retrieve a small icon within an action class method you might write:

Icon lineIcon = (Icon)getValue(SMALL_ICON);

boolean isEnabled()

Returns true if the action object is enabled and false otherwise.

void setEnabled(boolean state)

Sets the action object as enabled if the argument state is true and disabled if it is false. This operates on both the toolbar button and the menu item if they have been created using the same object.

void addPropertyChangeListener
(PropertyChangeListener listener)

This adds the listener passed as an argument that listens for changes to properties such as the enabled state of the object. This is used by a container for an action object to track property changes.

void removePropertyChangeListener(PropertyChangeListener listener)

This removes the listener passed as an argument. This is also for use by a Container object.
Of course, since the Action interface extends the ActionListener interface, it also incorporates the ActionPerformed() method that you are already familiar with.

So far, all we seem to have with this interface is a license to do a lot of work in implementing it but it's not as bad as that. The javax.swing package defines a class, AbstractAction, that already implements the Action interface. If you extend this class to create your own action class, you get a basic infrastructure for free. Let's try it out in the context of Sketcher.

Using Actions as Menu Items

This will involve major surgery on our SketchFrame class. Although we'll be throwing away all those fancy varieties of menu items we spent so much time putting together, at least you know how they work now, and we'll end up with much less code after re-engineering the class, as you'll see. As the saying goes, you've got to crack a few eggs to make a soufflé.

We'll go back nearly to square one and reconstruct the class definition. First we will delete a lot of code from the existing class definition. Comments show where we will add code to re-implement the menus using actions. Get your definition of SketchFrame to the following state:

// Frame for the Sketcher application
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class SketchFrame extends JFrame implements Constants {
  // Constructor
  public SketchFrame(String title) {
    setTitle(title);                            // Set the window title
    setJMenuBar(menuBar);                       // Add the menu bar to the window
    setDefaultCloseOperation(EXIT_ON_CLOSE);    // Default is exit the application

    JMenu fileMenu = new JMenu("File");            // Create File menu
    JMenu elementMenu = new JMenu("Elements");     // Create Elements menu
    fileMenu.setMnemonic('F');                     // Create shortcut
    elementMenu.setMnemonic('E');                  // Create shortcut

    // We will construct the file pull down menu here using actions...
    // We will add the types menu items here using actions...

    elementMenu.addSeparator();

    JMenu colorMenu = new JMenu("Color");          // Color sub-menu
    elementMenu.add(colorMenu);                    // Add the sub-menu

    // We will add the color menu items here using actions...

    menuBar.add(fileMenu);                         // Add the file menu
    menuBar.add(elementMenu);                      // Add the element menu
  }

  // We will add inner classes defining action objects here...

  // We will add action objects as members here...

  private JMenuBar menuBar = new JMenuBar();           // Window menu bar
  private Color elementColor = DEFAULT_ELEMENT_COLOR;  // Current element color
  private int elementType = DEFAULT_ELEMENT_TYPE;      // Current element type
}

Note that we have put the statement to set the default close operation as EXIT_ON_CLOSE back in so we won't need to call dispose() and exit() in the window event handler. The old inner classes have been deleted, as well as the fields storing references to menu items. All the code to create the menu items has been wiped as well, along with the code that added the listeners. We are ready to begin reconstruction. We can rebuild it, stronger, faster, better!

Defining Action Classes

We will need three inner classes defining actions, one for the File menu items, another for the element type menu items, and the third for element colors. We will derive all these from the AbstractAction class that already implements the Action interface. The AbstractAction class has three constructors:

Method

Description

AbstractAction()

Defines an object with a default name and icon.

AbstractAction(String name)

Defines an object with the name specified by the argument and a default icon.

AbstractAction(String name, Icon icon)

Defines an object with the name and icon specified by the arguments.

The AbstractAction class definition already provides the mechanism for storing action properties. For the last two constructors, the argument values that are passed will be stored using the standard keys that we described earlier. For the moment, we will only take advantage of the second constructor, and leave icons till a little later.

We can define the FileAction inner class as follows:

  class FileAction extends AbstractAction {    
    // Constructor
    FileAction(String name) {
      super(name);
    }

   // Constructor
    FileAction(String name, KeyStroke keystroke) {
      this(name);
      if(keystroke != null)
        putValue(ACCELERATOR_KEY, keystroke);
    }

    // Event handler
    public void actionPerformed(ActionEvent e) {
      // We will add action code here eventually...
    }
  }

We have two constructors. The first just stores the name for the action by calling the base class constructor. The second stores the name by calling the first constructor and then stores the accelerator keystroke using the appropriate key if the argument is not null. Calling the other constructor rather than the base class constructor is better here, in case we add code to the other constructor later on (as we shall!).

Since our class is an action listener, we can implement the actionPerformed() method in it. We don't yet know what we are going to do with the File menu item actions, so we will leave it open for now and let the actionPerformed() method do nothing. Add this inner class to SketchFrame where the comment indicates.

The SketchFrame class will need a data member of type FileAction for each menu item we intend to add, so add the following statement to the SketchFrame class definition:

  // File actions
  private FileAction newAction, openAction, closeAction,
                     saveAction, saveAsAction, printAction;

We can define an inner class for the element type menus next:

class TypeAction extends AbstractAction {    
  TypeAction(String name, int typeID) {
    super(name);
    this.typeID = typeID;
  }
  
  public void actionPerformed(ActionEvent e) {
    elementType = typeID;  
  }

  private int typeID;
}

Add this definition to the SketchFrame class following the previous inner class. The only extra code here compared to the previous action class is that we retain the typeID concept to identify the element type. This makes the listener operation simple and fast. Because each object corresponds to a particular element type, there is no need for any testing of the event – we just store the current typeID as the new element type in the SketchFrame class object. We won't be adding accelerator key combinations for type menu items so we don't need to provide for them in the class.

Add the following statement to the SketchFrame class for the members that will store references to the TypeAction objects:

// Element type actions
private TypeAction lineAction, rectangleAction, circleAction, curveAction;

The third inner class is just as simple:

// Handles color menu items
class ColorAction  extends AbstractAction {
  public ColorAction(String name, Color color) {
    super(name);
    this.color = color;
  }

  public void actionPerformed(ActionEvent e) {
    elementColor = color;
    // This is temporary – just to show it works
    getContentPane().setBackground(color);
  }

  private Color color;
}

We also use the same idea that we used in the listener class for the color menu items in the previous implementation of SketchFrame. Here we have a statement in the actionPerformed()method that sets the background color of the content pane to the element color. When you click on a color menu item, the background color of the content pane will change so you will be able to see that it works. We'll remove this code later.

Add the following statement to the SketchFrame class for the color action members:

// Element color actions
  private ColorAction redAction, yellowAction,
                      greenAction, blueAction;

We can try these action classes out now.

Try It Out – Actions in Action

All we need to do to create the menu items is use the add() method to add a suitable Action object to a menu. This all happens in the SketchFrame constructor – with the aid of a helper method that will economize on the number of lines of code:

public SketchFrame(String title) {
  setTitle(title);                            // Set the window title
  setJMenuBar(menuBar);                       // Add the menu bar to the window
  setDefaultCloseOperation(EXIT_ON_CLOSE);    // Default is exit the application

  JMenu fileMenu = new JMenu("File");         // Create File menu
  JMenu elementMenu = new JMenu("Elements");  // Create Elements menu
  fileMenu.setMnemonic('F');                  // Create shortcut
  elementMenu.setMnemonic('E');               // Create shortcut

  // Create the action items for the file menu
  newAction = new FileAction("New", KeyStroke.getKeyStroke('N',Event.CTRL_MASK ));
  openAction = new FileAction("Open", KeyStroke.getKeyStroke('O',Event.CTRL_MASK ));
  closeAction = new FileAction("Close");
  saveAction = new FileAction("Save", KeyStroke.getKeyStroke('S',Event.CTRL_MASK ));
  saveAsAction = new FileAction("Save As...");
  printAction = new FileAction("Print",
KeyStroke.getKeyStroke('P',Event.CTRL_MASK ));
// Construct the file pull down menu addMenuItem(fileMenu, newAction); addMenuItem(fileMenu, openAction); addMenuItem(fileMenu, closeAction); fileMenu.addSeparator(); // Add separator addMenuItem(fileMenu, saveAction); addMenuItem(fileMenu, saveAsAction); fileMenu.addSeparator(); // Add separator addMenuItem(fileMenu, printAction); // Construct the Element pull down menu addMenuItem(elementMenu, lineAction = new TypeAction("Line", LINE)); addMenuItem(elementMenu, rectangleAction = new TypeAction("Rectangle",
RECTANGLE));
addMenuItem(elementMenu, circleAction = new TypeAction("Circle", CIRCLE)); addMenuItem(elementMenu, curveAction = new TypeAction("Curve", CURVE)); elementMenu.addSeparator(); JMenu colorMenu = new JMenu("Color"); // Color sub-menu elementMenu.add(colorMenu); // Add the sub-menu addMenuItem(colorMenu, redAction = new ColorAction("Red", Color.red)); addMenuItem(colorMenu, yellowAction = new ColorAction("Yellow", Color.yellow)); addMenuItem(colorMenu, greenAction = new ColorAction("Green", Color.green)); addMenuItem(colorMenu, blueAction = new ColorAction("Blue", Color.blue)); menuBar.add(fileMenu); // Add the file menu menuBar.add(elementMenu); // Add the element menu }

We have added four blocks of code. The first two are for the file menu, one creating the action object and the other calling a helper method, addMenuItem(), to create the menu items. The other two are for the element type and color menus. We create the action items for these menus in the arguments to the helper method calls. It's convenient to do this, as the constructor calls are relatively simple.

The helper method will add an item specified by its second argument to the menu specified by the first. By declaring the second argument as type Action, we can pass a reference to an object of any class type that implements the Action interface, so this includes any of our action classes. Here's the code:

private JMenuItem addMenuItem(JMenu menu, Action action) {
  JMenuItem item = menu.add(action);                  // Add the menu item

  KeyStroke keystroke = (KeyStroke)action.getValue(action.ACCELERATOR_KEY);
  if(keystroke != null)
    item.setAccelerator(keystroke);
  return item;                                        // Return the menu item
}

As you can see, the method takes care of adding the accelerator key for the menu item if one has been defined for the Action object. If there isn't one, the getValue() method will return null, so it's easy to check. We don't need access to the menu item that is created in the method at the moment since it is added to the menu. However, it is no problem to return the reference from the method and it could be useful if we wanted to add code to do something with the menu item at some point.

If you compile and run Sketcher, you will get a window that looks like this:

How It Works

We create an Action object for each item in the file menu. We then call our private addMenuItem() method for each item in turn to create the menu items corresponding to the Action objects, and add them to the file menu. The addMenuItem() method automatically adds an accelerator key for a menu item if it exists in the Action object. We declare the addMenuItem() method as private because it has no role outside of the SketchFrame class and therefore should not be accessible.

The items for the other menus are created in the same way using the addMenuItem() method. We create the Action objects in the expressions for the arguments to the method, as they are relatively simple expressions. Because we store the references to the Action objects, they will be available later when we want to create toolbar buttons corresponding to the menu items. Note that we have omitted the accelerators for the Elements menu items here on the grounds that they were not exactly standard or convenient.

You may be wondering at this point why we have to set the accelerator key for a menu item explicitly, and why an accelerator key stored within an Action object is not added to the menu item automatically. There's a very good reason for not having the add() method automatically set an accelerator key from an Action object. Pressing an accelerator key combination would be the equivalent of clicking any item created from a corresponding Action object. This could be a toolbar button and a menu item. Thus if the accelerator keys were automatically set for both components, you would get events from both components when you press the accelerator key combination – not exactly what you would want as each action would then be carried out twice!

If you try out the color menus you should see the background color change. If it doesn't there's something wrong somewhere.

Now we have the menus set up using action objects, we are ready to tackle adding a toolbar to our application.

Previous Next
JavaScript Editor Java Tutorials Free JavaScript Editor