Jive Community Forums

 

  JavadesktopFAQ JavadesktopWIKI JavadesktopBLOG JavadesktopPROJECTS JavadesktopHOME javadotnet javadesktopFORUMS javadesktopARTICLES

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:

What is an Action?

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.

Creating Menus and Tool Bars from Actions

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.

Presenting the Easy Actions Framework

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.

Creating a Simple Command Action

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.

Creating Tool Bars, Menus and Popup Menus from Actions

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.

Conclusion

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