An Easy Architecture for Managing Actions
Simplifying the Creation of Tool Bars and
Menus...
by Mark Davidson
June 12, 2003
This article presents an architecture that simplifies the construction and
management of Swing's action-based components. The architecture allows you
to easily create tool bars, menus, and popup menus from an XML configuration
file as well as simplifies the process of connecting these components to your
application.
Introduction
When constructing a large application with deep menu
trees, many tool bars and popup menus, ensuring that
the properties are set correctly on each control is
quite tedious, error prone and somewhat fragile when
requirements change. An architectural strategy for
managing the user commands becomes necessary.
We present an actions framework that extends the javax.swing.Action interface.
The framework uses an external declarative XML document
to describe the properties and arrangement of Actions.
The XML document is used by a factory class to construct
containers for actions, such as menus, tool bars and
popup menus. The framework also extends the functionality
of Swing Actions by adding support for
toggle (on and off state) actions and radio (mutually
exclusive selection) actions.
A text editor demo is built as a sequence of incremental
steps for mastering this actions framework. The resulting
text editor is a professional-quality application where
all actions are shared among components and the availability
of an action is dictated by the current state of the
application. The runtime framework, the demo and the
source code is available for download.
The rest of this article has the following sections:
A user action translates a command for example,
a mouse click on a menu item or tool bar button into
a callback that executes some functionality. Classes
that extend the javax.swing.Action interface
represent an abstraction of a user command as a collection
of properties and some code that is executed when the
action is fired. For example, a "New" action might
have the value of "New Document" for its text property
and "Control-N" for its accelerator key property and
would contain a method for creating a new document.
When the "New" action is fired, either with a mouse
click or the keyboard, a new document is created and
set as the current document.
An Action may be bound to a specific
component like a JButton or JMenuItem and
may be shared among multiple components. Sharing an
action among multiple components has some distinct
advantages. The components inherit the properties from
the action this ensures the consistent use of
properties throughout your application. Furthermore,
when the properties of an action change, that change
is propagated to all components that share the action.
For example, if a button and a menu item share a "New" action,
then disabling the "New" action (setting the "enabled" property
to false) simultaneously disables both the button and
menu item.
If you have further questions about the nature of
actions and how they work, see How
to Use Actions in The
Swing Tutorial.
Currently, if you want to create a menu or a tool
bar from actions, you create a subclass of AbstractAction and
assign properties to the action using name-value pairs.
An instance of that action is then used to construct
a menu item or a tool bar button. Here is an example
of how this might be implemented:
Action fooAction = new AbstractAction {
public AbstractAction() {
super("Foo 1");
putValue(Action.MNEMONIC_KEY, "1");
}
public void actionPerformed() {
// do action command
}
};
// Create menu bar
JMenuBar menuBar = new JMenuBar();
JMenu menu1 = new JMenu("Foo");
menu1.setMmenonic('F');
JMenuItem menuItem = new JMenuItem(fooAction);
menu1.add(menuItem);
...
// Create tool bar
JToolBar toolbar = new JToolBar();
toolbar.add(fooAction);
...
This starts to get really tedious and error prone
as menu items are added, rearranged, or submenus are
added.
Using the actions framework, you use XML to specify
the properties of the actions, which components use
them, and where they are placed. Here is an XML snippet
that defines two actions, "Foo" and "Foo 1" and places "Foo" in
both the main menu and the tool bar and "Foo 1" in
the tool bar only:
<action id="foo-menu-command"
name="Foo"
mnemonic="F"/>
<action id="foo1-command"
name="Foo 1"
mnemonic="1"/>
<action-list id="main-menu">
<action-list id="foo-menu" idref="foo-menu-command">
<action idref="foo1-command"/>
...
</action-list>
</action-list>
<action-list id="main-toolbar">
<action idref="foo1-command"/>
...
</action-list>
Creating a menu or tool bar is now a simple matter
of passing an identifier to a factory class that contructs
the component and registers the action with a method
that handles the callback.
JMenuBar menubar = UIFactory.getInstance().createMenuBar("main-menu");
JToolBar toolbar = UIFactory.getInstance().createToolBar("main-toolbar");
ActionManager manager = ActionManager.getInstance();
manager.registerCallback("foo1-command", new FooController(), "handleFoo");
This architecture removes the definition of actions
and how they are used from your code and puts it into
XML resource files. This arrangement has many advantages.
Action definitions can be easily re-used in many applications.
Also, the syntax makes it easy to achieve deep nesting
of menu items. Most importantly, this framework simplifies
coding since it takes care of the details of creating
the actions.
The architectural framework makes it easy to define
and use actions. The properties of the actions, like text, icon, mnemonic and accelerator,
are specified in a well-formed XML document. The attributes
and elements must conform to the syntax described by
the action-set.dtd document.
There are three main elements used to specify an
action in an action XML document:
| Element |
Description |
| action-set |
The document root that contains a set of actions
and action-lists. |
| action |
Represents the properties of a javax.swing.Action,
such as text, icon, mnemonic or accelerator. |
| action-list |
Represents lists and trees of actions that
can be used to construct user interface components
like tool bars, menus and popups. |
All the elements must also have a unique identifier the
id attribute that is used by the classes in
the framework to reference these elements.
The XML schema doesn't specify the semantics of the
actions. The functionality of the action must be implemented
within the application. Separating the properties from
the semantics of an action allows the presentation
aspects of the application to be easily changed without
rebuilding the application. This separation also makes
it easy to localize the application or partition the
responsibility of the presentation aspect of the application
to another team. Also, an XML action document may be
reused in other applications.
The actions framework includes two public classes
that use XML action documents:
ActionManager Manages
the XML action documents, and may reference individual
actions for callback method registration and setting
properties.
UIFactory Uses
the ActionManager to create
action-based containers
like menu bars, menus,
tool bars and popup menus.
The next few sections outline the steps to accomplish
some basic tasks using the actions framework.
The framework may be used as a substitute for creating
Swing Actions. A few basic steps are required
to accomplish this:
- Define the properties of the actions in an XML
document.
- Load the document into the application's ActionManager.
- Register the callback method for the action.
- Create components from the actions.
The following example demonstrates creating an action,
registering a callback method, and creating a control
from that action.
Step 1: Define the Action properties
Create an XML representation of the action properties
based on the action-set.dtd.
To create an action identified as "new-command" with
the name "New" and other properties, the
XML snippet would look like:
<action id="new-command"
name="New"
mnemonic="N"
smicon="/toolbarButtonGraphics/general/New16.gif"
icon="/toolbarButtonGraphics/general/New24.gif"
accel="control N"
desc="Create a new object"/>
The id attribute must be unique for each
element. The icons are represented as relative paths
to the icons within the class path. The action-set.dtd document
contains a full description of the action element attributes.
Step 2: Load the document into the ActionManager
Use the loadActions method on the ActionManager class
to load the XML action document into the ActionManager.
ActionManager.loadActions(getClass().getResource("myactions.xml"));
Once loaded, all the actions and action-lists can
be referenced from the ActionManager using the unique
id defined for that element.
Note: The XML action documents must be
loaded before the creation of any components that use
the action.
Step 3: Register the callback method
Create a callback handler for the action. The method
signature must be public and have no parameters. The
method may be in its own class or it can be part of
a larger controller class that could contain the necessary
state to perform the action. However, the class enclosing
the callback method must also be public:
public class ActionController {
public void handleNew() {
System.out.println("A New Document");
}
....
}
The next step is to register the callback method
with the action identifier using the ActionManager.
To bind the "New" action to the "handleNew" method
use the registerCallback method on the
ActionManager instance:
manager.registerCallback("new-command", new ActionController(), "handleNew");
The handleNew method on the ActionController is
called when a control that used the "new-command" action
is fired.
Step 4: Create components from the Actions
Actions managed by the ActionManager may be retrieved
using the unique id. The action can then be attached
to a component either by passing the action to the
constructor for the component, or by using the setAction method
on the component. For example, to create a JButton from
the "new-command" action, use the ActionManager's getAction method:
Action action = manager.getAction("new-command");
if (action != null) {
JButton button = new JButton(action);
...
}
The example code in ActionDemo0.java shows
how all these steps produce a trivial button based
on an action using the actions framework. Clicking
the new button sends the string "A New Document" to
standard output. If you are executing the JavaTM Web
Start version then you must enable the console to see
any output.
A Java Web Start version of the demo can be executed by clicking the following
link: ActionDemo0.jnlp. In order to execute
the demo, you must have Java
Web Start installed.
Creating new individual components using an XML actions
document and ActionManager may seem like overkill for
small components. The next few sections demonstrate
how the actions framework simplifies the architecture
of an application as it scales.
The actions architecture allows you to conveniently
define lists and trees of actions that can be realized
as action containers like menus, tool bars and popups.
Use the action-list element in the XML actions
document to define lists of actions. The order of the
actions within a list reflects the order of components
within the container. Each action list must have a
unique identifier. For example, a simple tool bar may
look like this:
<action-list id="main-toolbar">
<action idref="new-command"/>
<action idref="open-command"/>
<action idref="save-command"/>
...
</action-list>
The idref attribute refers to existing actions
in the XML actions document. The action and action-list
elements support the inline definition of action properties.
Inline action definitions may allow the redefinition
of action properties. The same list implemented with
inline actions would look like:
<action-list id="main-toolbar">
<action id="new-command" name="New" mnemonic="N" accel="control N"/>
<action id="open-command" name="Open" mnemonic="O" desc="Opens a document"/>
<!-- The following command demonstrates redefining a property. -->
<action idref="save-command" desc="Save the foobar document"/>
...
</action-list>
Hierarchical menus are represented as trees that are implemented as lists of
lists. This syntax makes it quite easy to create multi-level nested menus by
creating action-lists within action-lists.
<action-list id="main-menu">
<action-list id="file-menu" idref="file-menu-command">
<action-list id="new-sub-menu" name="New..." mnemonic="N"/>
<action idref="new-browser-command"/>
<action idref="new-browser-tab-command"/>
<empty/>
...
</action-list>
<action idref="open-command"/>
<action idref="save-command"/>
...
</action-list>
<action-list id="view-menu" idref="view-menu-command">
...
</action-list>
<action-list id="help-menu" idref="help-menu-command">
...
</action-list>
You may have noticed that the submenu definition used the inline version
of the action-list. Also, the empty element denotes a separation of
elements.
Using the UIFactory
The UIFactory has
a series of "create" methods methods that take an action-list id as a parameter
and return a container of user actions like tool bars, menus and popup menus.
These containers may be added to the application's frame or panel.
JMenuBar menubar = factory.createMenuBar("main-menu");
if (menubar != null) {
getContentPane().setJMenuBar(menubar);
}
Currently, the UIFactory only supports the construction of JMenu, JMenuBar, JPopupMenu and JToolBar but
it may be subclassed to support the creation of any type of action container.
Also, UIFactory has a series of protected "configure" methods. These methods
may be overloaded to custom configure the components that are added to the
containers. When overloading these methods, you must ensure that the super
class method is called first.
A complete example which uses the ActionManager and UIFactory to create a
menu bar and tool bar would look like:
public static void main(String[] args) {
// load the Actions
ActionManager.loadActions(getClass().getResource("myactions.xml"));
// Register the callbacks for the loaded Actions.
ActionHandler handler = new ActionHandler();
manager.registerCallback("actionID", handler, "methodName");
// Create the toplevel frame
JFrame frame = new JFrame();
// Create the toplevel menu
frame.setJMenuBar(UIFactory.createMenuBar("main-menu"));
// Create the tool bar
frame.getContentPane().add(BorderLayout.NORTH,
UIFactory.createToolbar("main-toolbar"));
// Pack and show the frame
frame.pack();
frame.setVisible(true);
}
The ActionDemo1.java example demonstrates
a trivial text editor with a menu, a tool bar, and a popup menu constructed
from the actions and action-lists in the actions-demo.xml document.
The popup menu uses the same action-list as the tool bar and can be invoked
by right clicking with the mouse on the text pane. None of the actions are
registered with callback methods. All these action containers share the same
set of actions so their properties are identical.
A Java Web Start version of the demo can be executed by clicking the following
link: ActionDemo1.jnlp. In order to execute
the demo, you must have Java
Web Start installed.
Adding Support for Toggle or Radio Actions
The framework extends the functionality of a Swing Action by providing support
for toggle and radio actions. A toggle action is an action that can exist in
one of two states: selected or deselected (on or off). For example, a toggle
action can be used as the basis of a control like a check menu item or a toggle
button that can show or hide a status bar.
A radio action is group of toggle actions that form a set of mutually exclusive
actions. If one of the actions in the group is selected then the rest of the
actions are deselected. For example, in an application that supports rich text
editing, the controls for left, center and right justification of a paragraph
may be implemented as a group of mutually exclusive tool bar buttons or a group
of radio button menu items.
To create a toggle or radio action you must first create an XML representation
of the action properties in the same manner as a standard command action but
set the value of the type attribute to "toggle". If the type attribute
doesn't exist, its default value is "single". Both toggle and radio actions
are defined the same way.
<action id="align-right-command"
type="toggle"
name="Right Align"
mnemonic="R"
smicon="/toolbarButtonGraphics/text/AlignRight16.gif"
icon="/toolbarButtonGraphics/text/AlignRight24.gif"
desc="Adjust the placement of the text along the right side"/>
...
<action id="view-status-command"
type="toggle"
name="View Status Bar"
mnemonic="S"
smicon="/toolbarButtonGraphics/general/Status16.gif"
icon="/toolbarButtonGraphics/general/Status24.gif"
desc="Shows or hides the status bar"/>
Create the arrangement of the actions within the action-list elements. To
create a set of mutually exclusive radio actions place a set of toggle actions
within a group element. In the following example, the "view-status-command" is
implemented as a toggle action and the "alignment" commands form a set of mutually
exclusive radio actions.
<action-list id="main-toolbar">
<action idref="new-command"/>
...
<empty/>
<group id="align">
<action idref="align-left-command"/>
<action idref="align-center-command"/>
<action idref="align-right-command"/>
</group>
<empty/>
<action idref="view-status-command"/>
...
</action-list>
The id of the group element must be unique within the enclosing action-list
element. Grouped toggle actions within an action-list can only belong to one
group. In other words, all toggle actions within an action-list with multiple
groups must be unique.
Use ActionManager's loadActions method to load the document.
Use the "create" methods from UIFactory to create the action containers. Both
of these steps are described in the previous sections.
Create a callback handler for toggle actions. The callback method signature
must take a boolean parameter that represents the selection state transition
of the toggle action. When the toggle action is selected the value of the boolean
parameter passed to the method is "true". The parameter is false when the toggle
action is deselected.
public void handleViewStatus(boolean state) {
statusBar.setVisible(state);
}
Register the callback method with the action using the ActionManager:
manager.registerCallback("view-status-command", controller, "handleViewStatus");
The ActionDemo2.java example extends
the text editor demo by registering the callback methods on the actions. All
the callback methods are placed in the internal class ActionController.
Note: For the purposes of this demo, the save and justification actions
have not implemented. The "view-status-command" is actually implemented by
overloading the "history-command".
A custom StatusBar class is included as a demonstration of a
toggle action. The StatusBar component has an additional feature that displays
the description of an action when it detects a mouse-entered event over a component
that was created from that action. Adding support for displaying the action
description when key events are detected is left as a exercise to the reader.
A Java Web Start version of the demo can be executed by clicking the following
link: ActionDemo2.jnlp. In order to execute
the demo, you must have Java
Web Start installed.
Using actions can simplify user interfaces when there are multiple components
that implement the same functionality. The actions framework uses actions to
simplify the construction of menus and tool bars by taking care of the details.
The easy actions framework uses the Java XML parsing APIs so it requires Java
2 Platform, Standard Edition, version 1.4 or later. The complete bundle
includes the action framework, documentation, demos and source can be downloaded
from the following zip file: xml-actions.zip
The distribution contains the following files:
xml-actions.jar This is the ActionManager/UIFactory
runtime. Distribute this jar file with your applications that use this framework.
actions-demo.jar The demo classes that were
discussed in this article.
jlfgr-1_0.jar The icons used in the demo actions
document from Java
Look and Feel Graphics Repository.
The properties and arrangement of the actions within the XML document can be
edited and the changes can be seen without having to recompile any classes.
These classes are designed to simplify the construction of rich action-oriented
user interfaces. We hope that you find them useful for use in your own
applications.
I would like to thank Scott Violet and Hans Muller for their
valuable feedback in developing this framework
Post
a Comment
|