HomeAPIGuideSF Project Home

JAPI Guide: Swing Actions

JAPI Swing Actions Introduction

The class javax.swing.Action is probably the most underestimated Swing entity ever.

How do Actions help with Swing GUI programming?

They help alot. Let's start at the beginning. You want to create a Swing UI application. For user actions like New or Open you want to provide a JMenu named "File" with the corresponding JMenuItems. The same actions should also be reachable via JButtons in a JToolBar. The menu items and the toolbar buttons should have the same keyboard shortcut, the same icon, the same label (except that the toolbar buttons shouldn't display their label), the same mnemonic and the same tooltip.

Step 0: Plain code

We take an example application with some basic actions that should look like this:

Screenshots of sample applicationFirst screenshot of sample applicationSecond screenshot of sample application

Without actions, your code might look like this:

    ImageIcon iconNew = new ImageIcon("resources/new.png");
    JMenuItem miNew = new JMenuItem("new", iconNew);
    miNew.addActionListener(new ActionListener() {
        public void actionPerformed(final ActionEvent e) {
            // event handler code
        }
    });
    miNew.setAccelerator(getKeyStroke("ctrl pressed N"));
    miNew.setMnemonic(VK_N);
    miNew.setToolTipText("Creates a new document.");
    menuFile.add(miNew);

    JButton buNew = new JButton(iconNew);
    buNew.addActionListener(new ActionListener() {
        public void actionPerformed(final ActionEvent e) {
            // same event handler code as above
        }
    });
    buNew.setMnemonic(VK_N);
    buNew.setToolTipText("Creates a new document.");
    toolBar.add(buNew);
        

This kind of code is not very convenient. It is very redundant. Fixes and changes in the UI part need to be done at more than a single place. And anonymous classes are bloat code, remember, an anonymous class in your code adds two classes to the binary - in this example it's 4 additional classes just for handling one application event. If we look at a normal application, we'd have "new", "open", "save", "save as", "close", "quit", "about", "help", "cut", "copy", "paste" and for sure some more actions, but if we just take these 11 that would mean 44 additional classes just for handling the ActionEvents: 11 kinds of events, * 2 because we have a button and a menu item, * 2 because an anonymous class is technically 2 classes.

Last but not least, imagine you need to disable the action. With this kind of code you have to track all user interface elements, in this case the button and the menu item, and disable / enable them all separately.

Step 1: Reusing ActionListener

A first improvement could be to use the same ActionListener instance for both, the menu item and the toolbar button. This would already reduce the number of action event handlers by 50%:

    final ActionListener alNew = new ActionListener() {
        public void actionPerformed(final ActionEvent e) {
            // event handler code
        }
    };
    ImageIcon iconNew = new ImageIcon("resources/new.png");
    JMenuItem miNew = new JMenuItem("new", iconNew);
    miNew.addActionListener(alNew);
    miNew.setAccelerator(getKeyStroke("ctrl pressed N"));
    miNew.setMnemonic(VK_N);
    miNew.setToolTipText("Creates a new document.");
    menuFile.add(miNew);

    JButton buNew = new JButton(iconNew);
    buNew.addActionListener(alNew);
    buNew.setMnemonic(VK_N);
    buNew.setToolTipText("Creates a new document.");
    toolBar.add(buNew);
        

This does not yet solve the redundancy of the UI element attributes like icon, tooltip or mnemonic, nor the need to still care about both UI elements when disabling or enabling the action.

Step 2: Using AbstractAction

Now let's have a look at the interface Action. What's it? Not much, but useful. It's nothing but an extended ActionListener that additionally defines an interface for enabling and property handling. AbstractAction is an implementation of Action and all its methods except, of course, actionPerformed(). Also, Action defines some interesting keys for properties / attributes that are commonly used for UI elements that trigger ActionEvents: ACCELERATOR_KEY for the key stroke, LONG_DESCRIPTION for context sensitive help, MNEMONIC_KEY for the mnemonic, NAME for the plain display label, SHORT_DESCRIPTION for the tooltip text and SMALL_ICON for, well, the icon of course.

Not only are constructors of classes like JButton and JMenuItem overloaded to take Action arguments, but also are the add() methods of JMenu and JToolBar, you don't even need to care about instanciating JButton or JMenuItem.

With this knowledge, we could use an AbstractAction instead of an ActionListener:

    final Action aNew = new AbstractAction("new", new ImageIcon("resources/new.png")) {
        public void actionPerformed(final ActionEvent e) {
            // event handler code
        }
    };
    aNew.putValue(ACCELERATOR_KEY, getKeyStroke("ctrl pressed N"));
    aNew.putValue(MNEMONIC_KEY, VK_N);
    aNew.putValue(SHORT_DESCRIPTION, "Creates a new document.");

    menuFile.add(aNew);
    toolBar.add(aNew);
        

Compare this with the first example: Hell, is this code short! It's just half the number of lines of the first example.

How does JAPI make Actions even more useful?

We're still using an anonymous class. JAPI provides a few classes that eliminate the need of anonymous classes or any additional classes for action event handling at all. The JAPI classes for this are: ActionFactory, ReflectionAction, ToggleAction and DummyAction. That's just four classes, giving you a class count break even point of two actions!

But that's not all, JAPI also cares about moving the action properties to preferences and properties files, thus caring about Internationalization and Localization. You could even provide a user interface that allows the user to configure specific aspects of actions, for instance the key strokes or the icons.

ReflectionAction

Step 3: Using ReflectionAction

ReflectionAction is an implementation of AbstractAction that uses Reflection to invoke a method. Imagine the method is called "newDocument()" at the same object that creates the UI, then the code using ReflectionAction would look like this:

    final Action aNew = new ReflectionAction("new", new ImageIcon("resources/new.png"), "newDocument", this);
    aNew.putValue(ACCELERATOR_KEY, getKeyStroke("ctrl pressed N"));
    aNew.putValue(MNEMONIC_KEY, VK_N);
    aNew.putValue(SHORT_DESCRIPTION, "Creates a new document.");

    menuFile.add(aNew);
    toolBar.add(aNew);
        

The code gets shorter and shorter. You've just traded all your anonymous class imlementations of ActionListener, Action or AbstractAction (remember: each anonymous class means two compiled classes) for a single additional class named ReflectionAction and thus significantly reduced your code size.

Isn't Reflection slow? Well, speed is relative. For one thing, Reflection is even used in Application Servers. And for another thing, do you really believe the user can trigger actions at a speed where he will notice that your program uses Reflection to handle the ActionEvents?

Step 4: Add internationalization and localization

To understand where ActionFactory helps, we will extend our code with I18N (internationalization) and L10N (localization). I18n means we change our code in a way that l10n is possible. And l10n means we adapt our program for a certain locale specific environment, which mostly is the language of the user interface.

    final ResourceBundle bundle = getBundle("mypackage.action");
    final Action aNew = new ReflectionAction(
            bundle.getString("newDocument.text"),
            new ImageIcon(bundle.getString("newDocument.icon"),
            "newDocument", this);
    aNew.putValue(ACCELERATOR_KEY, getKeyStroke(bundle.getString("newDocument.accel"));
    aNew.putValue(MNEMONIC_KEY, getKeyStroke(bundle.getString("newDocument.mnemonic")).getKeyCode());
    aNew.putValue(SHORT_DESCRIPTION, bundle.getString("newDocument.shortdescription"));

    menuFile.add(aNew);
    toolBar.add(aNew);
        

All values are read from a properties file named "mypackage/action*.properties" now. Such properties files could look like this:

File mypackage/action.properties

    newDocument.text=new
    newDocument.icon=resources/new.png
    newDocument.accel=ctrl pressed N
    newDocument.mnemonic=N
    newDocument.shortdescription=Creates a new document
        

File mypackage/action_de.properties

    newDocument.text=neu
    newDocument.shortdescription=Erzeugt ein neues Dokument
        

Action Factory

Step 5: using ActionFactory

ActionFactory is a Factory for Actions. It will search a default and optionally additional property bundles for the corresponding key / value pairs. With the same bundles, the i18n/l10n handling and usage of Reflection for the action method invocation will look like this:

    final ActionFactory actionFactory = ActionFactory.getFactory("mypackage");
    final Action aNew = actionFactory.createAction(false, "newDocument", this);
    menuFile.add(aNew);
    toolBar.add(aNew);
        

Now THAT is short code.

Step 6: using ActionFactory for complete menus and toolbars

ActionFactory can do even more for you. It can create complete menus and toolbars. Oh yes it does! All you have to do is follow some simple naming conventions about your properties files.

    final ActionFactory actionFactory = ActionFactory.getFactory("mypackage");
    final JFrame frame = new JFrame(actionFactory.getString("appWindow.title"));
    frame.setJMenuBar(actionFactory.createMenuBar(true, "app", this));
    frame.add(actionFactory.createToolBar("app"), NORTH);
        

Final example: Application skeleton

Java Source: src/ex/Application.java

package ex;

import java.awt.BorderLayout;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JFrame;
import net.sf.japi.swing.ActionFactory;
import net.sf.japi.swing.ActionMethod;

/** Example application. */
public class Application extends WindowAdapter {

   
/** The application frame. */
   
private JFrame frame;

   
/** Main program.
     *
@param args command line arguments (currently ignored)
     */
   
public static void main(final String... args) {
       
//noinspection ResultOfObjectAllocationIgnored
       
new Application();
   
}

   
private ActionFactory actionFactory = ActionFactory.getFactory("ex");

   
public Application() {
       
frame = new JFrame(actionFactory.getString("appWindow.title"));
        frame.setJMenuBar
(actionFactory.createMenuBar(true, "app", this));
        frame.add
(actionFactory.createToolBar("app"), BorderLayout.NORTH);
        frame.pack
();
        frame.addWindowListener
(this);
        frame.setVisible
(true);
   
}

   
@ActionMethod public void fileNew() {
       
// Implement this method for creating a new file
   
}

   
@ActionMethod public void fileOpen() {
       
// Implement this method for opening an existing file
   
}

   
@ActionMethod public void fileSave() {
       
// Implement this method for saving the current document
   
}

   
@ActionMethod public void fileSaveAs() {
       
// Implement this method for saving the current document in a different file
   
}

   
@ActionMethod public void fileClose() {
       
// Implement this method for closing the current document
   
}

   
@ActionMethod public void fileQuit() {
       
// Change this method for asking whether to really quit the application
       
frame.dispose();
   
}

   
@ActionMethod public void editCut() {
       
// Implement this method for cutting (edit operation)
   
}

   
@ActionMethod public void editCopy() {
       
// Implement this method for copying (edit operation)
   
}

   
@ActionMethod public void editPaste() {
       
// Implement this method for pasting (edit operation)
   
}

   
/** {@inheritDoc} */
   
@Override public void windowClosing(final WindowEvent e) {
       
fileQuit();
   
}

}
// class ex.App

Default (English) bundle: src/ex/action.properties

appWindow.title=Example Application
app.menubar=file edit
file.menu=fileNew fileOpen fileSave fileSaveAs fileClose fileQuit
edit.menu=editCut editCopy editPaste
app.toolbar=fileNew fileOpen fileSave - editCut editCopy editPaste

file.text=File
file.mnemonic=F

fileNew.text=New
fileNew.mnemonic=N
fileNew.accel=ctrl pressed N
fileNew.shortdescription=Creates a new file
fileNew.icon=general/New16

fileOpen.text=Open...
fileOpen.mnemonic=O
fileOpen.accel=ctrl pressed O
fileOpen.shortdescription=Opens an existing file
fileOpen.icon=general/Open16

fileSave.text=Save
fileSave.mnemonic=S
fileSave.accel=ctrl pressed S
fileSave.shortdescription=Saves the current file
fileSave.icon=general/Save16

fileSaveAs.text=Save As...
fileSaveAs.mnemonic=A
fileSaveAs.accel=ctrl shift pressed S
fileSaveAs.shortdescription=Saves the current file under a new name
fileSaveAs.icon=general/SaveAs16

fileClose.text=Close
fileClose.mnemonic=C
fileClose.accel=ctrl pressed W
fileClose.shortdescription=Closes the current file

fileQuit.text=Quit
fileQuit.mnemonic=Q
fileQuit.accel=ctrl pressed Q
fileQuit.shortdescription=Quits the application

edit.text=Edit
edit.mnemonic=E

editCut.text=Cut
editCut.mnemonic=T
editCut.accel=ctrl pressed X
editCut.shortdescription=Cuts the current selection into the clipboard
editCut.icon=general/Cut16

editCopy.text=Copy
editCopy.mnemonic=C
editCopy.accel=ctrl pressed C
editCopy.shortdescription=Copies the current selection into the clipboard
editCopy.icon=general/Copy16

editPaste.text=Paste
editPaste.mnemonic=P
editPaste.accel=ctrl pressed V
editPaste.shortdescription=Pastes the current clipboard content over the current selection
editPaste.icon=general/Paste16

German bundle: src/ex/action_de.properties

file.text=Datei
file.mnemonic=D

fileNew.text=Neu
fileNew.mnemonic=N
fileNew.accel=ctrl pressed N
fileNew.shortdescription=Erzeugt eine neue Datei

fileOpen.text=Öffnen...
fileOpen.mnemonic=F
fileOpen.accel=ctrl pressed O
fileOpen.shortdescription=Öffnet eine bestehende Datei

fileSave.text=Speichern
fileSave.mnemonic=S
fileSave.accel=ctrl pressed S
fileSave.shortdescription=Speichert die aktuelle Datei

fileSaveAs.text=Speichern Als...
fileSaveAs.mnemonic=A
fileSaveAs.accel=ctrl shift pressed S
fileSaveAs.shortdescription=Speichert die aktuelle Datei unter einem neuen Namen

fileClose.text=Schließen
fileClose.mnemonic=C
fileClose.accel=ctrl pressed W
fileClose.shortdescription=Schließt die aktuelle Datei

fileQuit.text=Beenden
fileQuit.mnemonic=B
fileQuit.accel=ctrl pressed Q
fileQuit.shortdescription=Beendet das Programm

edit.text=Bearbeiten
edit.mnemonic=B

editCut.text=Ausschneiden
editCut.mnemonic=A
editCut.accel=ctrl pressed X
editCut.shortdescription=Verschiebt die aktuelle Auswahl in die Zwischenablage

editCopy.text=Kopieren
editCopy.mnemonic=K
editCopy.accel=ctrl pressed C
editCopy.shortdescription=Kopiert die aktuelle Auswahl in die Zwischenablage

editPaste.text=Einfügen
editPaste.mnemonic=E
editPaste.accel=ctrl pressed V
editPaste.shortdescription=Kopiert den aktuellen Zwischenablageninhalt über die aktuelle Auswahl

The source code distribution of JAPI of course also contains this example, along with a small build.xml to compile and run it.

SourceForge.net LogoSupport This ProjectValid HTML 4.01!Valid CSS! Feedback: webmaster
$Date: 2006-04-16 15:11:23 +0200 (Son, 16 Apr 2006) $