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 JMenuItem
s.
The same actions should also be reachable via JButton
s 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:
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.