CSE190L Notes for Wednesday, 5/16/07

We continued looking at variations of the Clicker program. First I said that we should fix a minor flaw in the program. If the user clicks on a position and then selects restore, the program draws an extra computer point after the restore. That's because we set up a timer to delay the computer's move for 2 seconds. If the user does a restore in that time period, we get the extraneous computer move. So we had to add a line of code to stop the timer in the restore action listener:

	public void actionPerformed(ActionEvent e) {
	    timer.stop();
            ....
        }
This led to another minor glitch. This stopped the extraneous point from being chosen, but it also left the user without the ability to select points after the restore. That's because the timer was also resetting our internal flag to allow user clicks. So we added an extra line of code at the end of the actionPerformed method to make sure the user can select points:

	public void actionPerformed(ActionEvent e) {
	    timer.stop();
            ....
	    clickOkay = true;
        }
This kind of minor tweaking of the user experience is common in GUI programming. It's difficult to anticipate all of these odd cases, which is why it's so important to do thorough testing of your program.

Then we added key accelerators to the menu items. A key accelerator defines a keyboard shortcut. The code for doing this is sufficiently obscure that I don't mind using a source like Core Java and just adapting some existing code. I added two new commands after constructing the menu items:

        save = new JMenuItem("Save");
        restore = new JMenuItem("Restore");
        save.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S,
                                                   InputEvent.CTRL_MASK));
        restore.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R,
                                                      InputEvent.CTRL_MASK));
The constant "VK_S" is short for "virtual keyboard S" and the constant "VK_R" is short for "virtual keyboard R". The constant "CTRL_MASK" indicates that we want a ctrl-s and ctrl-r, not a simple s and simple r. With these two lines of code added, we found that our menu items were displayed with these control characters listed as accelerators.

Then I said that I wanted to explore the use of Action objects to do some of this same functionality. Action objects are most useful when you intend to have several different controls using the same action. For example, you might have a menu option for save and a button for save. Action objects also simplify some of the work of setting up menus and other controls.

An Action object is like an ActionListener but with extra information about how the action will be presented to the user. Action is an interface and there is an AbstractAction class that does the bookkeeping work necessary to make it work. We currently use an anonymous inner class to construct two ActionListener objects for save and restore. When you move to using an Action object, the code tends to get long enough that it makes sense to define private inner classes. So I defined a new class for the save action and moved the code for actionPerformed into that class:

        private class SaveAction extends AbstractAction {
	    public void actionPerformed(ActionEvent e) {
                ...
            }
        }
With an Action object, you typically include a constructor that sets the configuration information for the action:

        private class SaveAction extends AbstractAction {
	    public SaveAction() {
	        super("Save");
            }

	    public void actionPerformed(ActionEvent e) {
                ...
            }
        }
We made similar changes to define a RestoreAction class. Then I changed our fields to be of type Action:

        private Action save;
        private Action restore;
And in the private method to set up the menu, we had to construct these objects by constructing instances of our new inner classes:

        save = new SaveAction();
        restore = new RestoreAction();
With the Action objects, it is much easier to construct controls like a menu. Normally we'd have to construct individual MenuItem objects and add them to the Menu, but with an Action object, you can simply add it to a Menu and Swing takes care of the rest. So our menu code became simpler:

        // construct a menu
        JMenuBar bar = new JMenuBar();
        JMenu menu = new JMenu("File");
        menu.add(save);
        menu.add(restore);
        bar.add(menu);
        frame.setJMenuBar(bar);
We ran this version of the program and it behaved like the previous version other than the accelerator key. I had commented out the call on setAccelerator because that is a method that is specific to the MenuItem class. With an Action object, you specify this kind of information by setting a property of the Action. We looked at the api documentation for the Action interface and found several named constants. I modified our save and restore actions to set an accelerator and a short description:
        private class SaveAction extends AbstractAction {
	    public SaveAction() {
	        super("Save");
                putValue(Action.SHORT_DESCRIPTION, "save current points");
                putValue(Action.ACCELERATOR_KEY, 
                         KeyStroke.getKeyStroke(KeyEvent.VK_S, 
                                                InputEvent.CTRL_MASK));
            }

	    public void actionPerformed(ActionEvent e) {
                ...
            }
        }
We found that this version included the accelerators keys, as it did before, and the short description showed up as a tool tip. Normally you have to set the tool tip yourself. The Action object is a nice alternative that sets it for you if you have properly configured the object.

We added two more settings, one for an icon and one for a mneumonic key:

        private class SaveAction extends AbstractAction {
	    public SaveAction() {
	        super("Save");
                putValue(Action.SHORT_DESCRIPTION, "save current points");
                putValue(Action.ACCELERATOR_KEY, 
                         KeyStroke.getKeyStroke(KeyEvent.VK_S, 
                                                InputEvent.CTRL_MASK));
                putValue(Action.SMALL_ICON, new ImageIcon("save.gif"));
                putValue(Action.MNEMONIC_KEY, new Integer('S'));
            }

	    public void actionPerformed(ActionEvent e) {
                ...
            }
        }
For the icon, I had to find an image to use. I copied the images used in the Notepad sample program from the jfc folder that we looked at last week. Adding these two lines of code caused the program to include icons and mnemonic keys.

The real payoff for the Action object comes when you want multiple controls. For example, our original program had two JButtons for save and restore. Using the action objects, it was easy to recreate this panel of buttons:

        // construct a panel
        JPanel p = new JPanel();
        p.add(new JButton(save));
        p.add(new JButton(restore));
        frame.add(p, BorderLayout.SOUTH);
We also used the Action objects to construct a tool bar:

        // construct a toolbar
        JToolBar tbar = new JToolBar();
        tbar.add(save);
        tbar.add(restore);
        frame.add(tbar, BorderLayout.NORTH);
The tool bar is a fairly interesting Swing component that can be moved around by the user and even moved off of the panel. We also constructed a popup menu. To make it work, we had to instruct our drawing panel to use that panel as its popup menu:

        // construct a popup menu
        pop = new JPopupMenu();
        pop.add(save);
        pop.add(restore);
        panel.setComponentPopupMenu(pop);
In the last few minutes of class, someone asked how to make this popup menu appear when the frame initially launches. To do that, we have to elevate the JPopupMenu item to a field:

        private JPopupMenu pop;
And we have to add a line of code to the start command of the frame to show this menu:

        public void run() {
            frame.setVisible(true);
            if (!userFirst)
                computerMove();
            clickOkay = true;
            pop.show(panel, 50, 50);
        }
This didn't work quite correctly on my Mac, but I think that's a Mac-specific issue. As soon as I moved my cursor into the popup menu, its choices became visible. I was able to fix this by including a call on pop.repaint after the call on pop.show.

This version of the program is really pretty silly. Who would really want to have these commands available from a menu, a popup menu, a tool bar and a set of buttons? But it shows off the power of Action objects.

Even though we made lots of changes, the only class that changed was ClickerController. I have included the final version of the class as handout 21.


Stuart Reges
Last modified: Fri May 18 10:01:54 PDT 2007