Chapter 12. Menus, Toolbars, and Actions

In this chapter:

 

12.1 Menus, toolbars, and actions overview

Drop-down menu bars, context-sensitive popup menus, and draggable toolbars have become commonplace in many modern applications. It is no surprise that Swing offers these features, and in this section we will discuss the classes and interfaces that underly them. The remainder of this chapter is then devoted to the stepwise construction of a basic text editor application to demonstrate each feature discussed here.

12.1.1 The SingleSelectionModel interface

abstract interface javax.swing.SingleSelectionModel

This simple interface describes a model which maintains a single selected element from a given collection. Methods to assign, retrieve, and clear a selected index are declared, as well as methods for attaching and removing ChangeListeners. Implementations are responsible for the storage and manipulation of the collection to be selected from, maintaining an int property representing the selected element, maintaining a boolean property specifying whether or not an element is selected, and are expected to fire ChangeEvents whenever the selected index changes.

12.1.2 DefaultSingleSelectionModel

class javax.swing.DefaultSelectionModel

This is the default implementation of SingleSelectionModel used by JMenuBar and JMenuItem. The selectedIndex property represents the selected index at any given time and is -1 when nothing is selected. As expected we can add and remove ChangeListeners, and the protected fireStateChanged() method is responsible for dispatching ChangeEvents whenever the selectedIndex property changes.

12.1.3 JMenuBar

class javax.swing.JMenuBar

JMenuBar is a container for JMenus layed out horizontally in a row, typically residing at the top of a frame or applet. We use the add(JMenu menu) method to add a new JMenu to a JMenuBar. We use the setJMenuBar() method in JFrame, JDialog, JApplet, JRootPane, and JInternalFrame to set the menu bar for these containers (recall that each of these containers implements RootPaneContainer, which enforces the definition of setJMenuBar()--see chapter 3). JMenuBar uses a DefaultSingleSelectionModel to enforce the selection of only one child at any given time.

A JMenuBar is a JComponent subclass and, as such, can be placed anywhere in a container just as any other Swing component (this functionality is not available with AWT menu bars).

 

Warning: JMenuBar defines the method setHelpMenu(JMenu menu) which is intened to mark a single menu contained in a JMenuBar as the designated ‘help’ menu. The JMenuBar UI delegate may be responsible for positioning and somehow treating this menu differently than others. Hoewever, this is not implemented as of Java 2 FCS, and generates an exception if used.

 

Note: One lacking feature in the current JMenuBar implementation, or its UI delegate, is the ability to easily control the spacing between its JMenu children. As of Java 2 FCS, the easiest way to control this is by overriding JMenuBar and manually taking control of its layout. By default JMenubar uses an x-oriented BoxLayout.

JMenuBar provides several methods to retrieve its child components, set/get the currently selected item, register/unregister with the current KeyBoardManager (see chapter 2, section 2.13), and the isManagingFocus() method which simply returns true to indicate that JMenuBar handles focus management internally. Public methods processKeyEvent() and processMouseEvent() are implemented only to satisfy the MenuElement interface (see below) requirements, and do nothing by default.

12.1.4 JMenuItem

class javax.swing.JMenuItem

This class extends AbstractButton (see chapter 4, section 4.1) and represents a single menu item. We can assign icons and keyboard mnemonics just as we can with buttons. A mnemonic is represented graphically by underlining the first instance of the corresponding character, just as it is in buttons. Icon and text placement can be dealt with identically to how we deal with this functionality in buttons.

We can also attach keyboard accelerators to a JMenuItem.(i.e. we can register keyboard actions with a JMenuItem -- see chapter 2 section 2.13). Every JComponent decendent inherits similar functionality. When assigned to a JMenuItem, the accelerator will appear as small text to the right of the menu item text. An accelerator is a key or combination of keys that can be used to activate a menu item. Contrary to a mnemonic, an accelerator will invoke a menu item even when the popup containing it is not visible. The only necessary condition for accelerator activation is that the window containing the target menu item is currently active. To add an accelerator corresponding to CT! RL+A we can do the following:

myJMenuItem.setAccelerator(KeyStroke.getKeyStroke(

KeyEvent.VK_A, KeyEvent.CTRL_MASK, false);

 

Note: JMenuItem is the only Swing component that graphically displays an assigned keyboard accelerator.

We normally attach an ActionListener to a menu item. As any button, whenever the menu item is clicked the ActionListener is notified. Alternatively we can use Actions (discussed below and briefly in 2.13) which provide a convenient means of creating a menu item as well as definining the corresponding action handling code. A single Action instance can be used to create an arbitrary number of JMenuItems and JButtons with identical action handling code. We will see how this is done soon enough. It suffices to say here that when an Action is disabled, all JMenuItems associated with that Action are disabled and, as buttons always do in the disabled state, appear grayed out.

As any AbstractButton decendent, JMenuItem fires ActionEvents and ChangeEvents and allows attachment of ActionListeners and ChangeListeners accordingly. JMenuItem will also fire MenuDragMouseEvents (see below) when the mouse enters, exits, is dragged, or a mouse button is released inside its bounds, and MenuKeyEvents whe! n a key is pressed, typed, or released. Both of these Swing-specific events will only be fired when the popup containing the corresponding menu item is visible. As expected, we can add MenuDragMouseListeners and MenuKeyEventListeners for notification of these events. Several public processXXEvent() methods are also provided to receive and respond to events dispatched to a JMenuItem, some of which are forewarded from the current MenuSelectionManager (see below).

12.1.5 JMenu

class javax.swing.JMenu

This class extends JMenuItem and is usually added to a JMenuBar or to another JMenu. In the former case it will act as a menu item which pops up a JPopupMenu containing child menu items. If a JMenu is added to another JMenu it will appear in that menu’s corresponding popup as a menu item with an arrow on its right side. When that menu item is activated by mouse movement or keyboard selection a popup will appear displaying its corresponding child menu items. Each JMenu maintains a topLevelMenu property which is false for sub-menus and true otherwise.

JMenu uses a DefaultButtonModel to manage its state, and it holds a private instance of JPopupMenu (see below) used to display its associated menu items when it is activated with the mouse or a keyboard mnemonic.

 

Note: Unlike its JMenuItem parent, JMenu specifically overrides setAccelerator() with an empty implementation to disallow keyboard accelerators. This is because it assumes that we will only want to activate a menu (i.e. display its popup) when it is already visible; and for this we can use a mnemonic.

We can display/hide the associated popup programmatically by setting the popupMenuVisible property, and we can access the popup using getPopupMenu(). We can set the coordinate location where the popup is displayed with setMenuLocation(). We can assign a specific delay time in milliseconds using setDelay() to specify how long a JMenu should wait before displaying its popup when activated.

We use the overloaded add() method to add JMenuItems, Components, Actions (see below) or Strings to a JMenu. (Adding a String simply creates a JMenuItem child with the given text.) Similarly we can use several variations of overloaded insert() and remove() methods to insert and remove existing children. JMenu also directly supports creation and insertion of separator components in its popup, using addSeparator(), which provides a convenient means of visually organizing child components into groups.

The protected createActionChangeListener() method is used when an Action is added to a JMenu to create a PropertyChangeListener for internal use in responding to bound property changes that occur in that Action (see below). The createWinListener() method is used to create an instance of the protected inner class JMenu.WinListener which is used to deselect a menu when its corresponding popup closes. We are rarely concerned with these methods, and only subclasses ! desiring a more complete customization will override them.

Along with event dispatching/handling inherited from JMenuItem, JMenu adds functionality for firing and capturing MenuEvents (see below) used to notify attached MenuListeners when its current selection changes.

UI Guideline : Flat and wide design

Recent research in usability has shown that menus with too many levels of hierachy don't work well. Features get buried too many layers deep. Some operating systems restrict menus to 3 levels i.e. the main menu bar, a pull down menu and a single walking pop-up menu.

A maximum 3 Levels would be appear to be a good rule of thumb. Don't be tempted to use popup menus to create a complex series of hierarchical choices. Keep menus flatter.

For each menu, another good rule of thumb is to provide 7 +/- 2 options. However, if you have too many choices, it is better to break this rule and go to 10 or more than to introduce additional hierarchy.

 

12.1.6 JPopupMenu

class javax.swing.JPopupMenu

This class represents a small window which pops up and contains a collection of components layed out in a single column by default using, suprisingly, a GridBagLayout (note that there is nothing stopping us from changing JPopupMenu’s layout manager). JPopupMenu uses a DefaultSingleSelectionModel to enforce the selection of only one child at any given time.

JMenu simply delegates all its add(), remove(), insert(), addSeparator(), etc., calls to its internal JPopupMenu. As expected, JPopupMenu provides similar methods. The addSeparator() method inserts an instance of the inner class JPopupMenu.Separator (a subclass of JSeparator -- discussed below). The show() method displays a JPopupMenu at a given position within the corrdinate system of a given component. This component is referred to as the invoker component, and JPopupMenu can be assigned an invoker by setting its invoker property. JComponent’s setVisible() method is overriden to display a JPopupMenu with respect to its current invoker, and we can change the location it will appear using setLocation(). We c! an! also control a JPopupMenu’s size with the overloaded setPopupSize() methods, and we can use the pack() method (similar to the java.awt.Window method of the same name) to request that a popup change size to the minimum required for correct display of its child components.

 

Note: JComboBox’s UI delegate uses a JPopupMenu subclass to display its popup list.

When the need arises to display our own JPopupMenu, it is customary, but certainly not necessary, to do so in response to a platform-dependent mouse gesture (e.g. a right-click on Windows platforms). Thus, the java.awt.event.MouseEvent class provides a simple method we can use in a platform-independent manner to check whether a platform-dependent popup gesture has occurred. This method, isPopupTrigger(), will return true if the MouseEvent it is called on represents the current operating system’s popup trigger gesture.

JPopupMenu has the unique ability to act as either a heavyweight or lighweight component. It is smart enough to detect when it will be displayed completely within a Swing container or not and adjust itself accordingly. However, there may be cases in which the default behavior may not be acceptable. Recall from chapter 2 that we must set JPopupMenu’s lightWeightPopupEnabled property to false to force it to be heavyweight and allow overlapping of other heavyweight components that might reside in the same container. Setting this property to true will force a JPopupMenu to remain lightweight. The static setDefaultLightWeightPopupEnabled() method serves the same purpose, but affects all JPopupMenu’s created from that point on (in the current implementation all popups existing before this method is called will retain their previous lightweight/heavyweight settings).

 

Bug Altert! Due to an AWT bug, all popups are forced into lightweight mode when displayed in modal dialogs (regardless of the state of the lightWeightPopupEnabled property).

The protected createActionChangeListener() method is used when an Action (see below) is added to a JPopupMenu to create a PropertyChangeListener for internal use in responding to bound property changes that occur in that Action.

A JPopupMenu fires PopupMenuEvents (discussed below) whenever it is made visible, hidden, and cancelled. As expected we can attatch PopupMenuListeners to capture these events.

12.1.7 JSeparator

class javax.swing.JSeparator

This class represents a simple separator component with a UI delegate responsible for displaying a horizontal or vertical line. We can specify which orientation a JSeparator should use by changing its orientation property. This class is most often used in menus and toolbars, however, it is a standard Swing component and there is nothing stopping us from using JSeparators anywhere we like.

We normally do not use JSeparator explicitly. Rather, we use the addSeparator() method of JMenu, JPopupMenu, and JToolBar. JMenu delegates this call to its JPopupMenu which, as we know, uses an instance of its own custom JSeparator subclass which is rendered as a horizontal line. JToolBar also uses its own custom JSeparator sub-class which has no graphical representation, and appears as just an empty region. Unlike menu separators, however, JToolBar’s separator allows explicit instantiation and provides a method for assigning a new size in the form of a Dimension.

 

UI Guideline : Use of a separator

Use a separator to group related menu choices and separate them from others. This gives better visual communication and better usability by providing a space between the target areas for groups of choices. This reduces the chance of an error when making a selection with the mouse.

 

12.1.8 JCheckBoxMenuItem

class javax.swing.JCheckBoxMenuItem

This class extends JMenuItem and can be selected, deselected, and rendered identical to JCheckBox (see chapter 4). We use the isSelected()/setSelected() or getState()/setState() methods to determine/set the selection state respectively. ActionListeners and ChangeListeners can be attached to a JCheckBoxMenuItem for notification about changes in its state (see JMenuItem discussion for inherited functionality). We often use JCheckBoxMenuItems in ButtonGroups to enforce the selection of only one item in a group at any given time.

12.1.9 JRadioButtonMenuItem

class javax.swing.JRadioButtonMenuItem

This class extends JMenuItem and can be selected, deselected, and rendered identical to JRadioButton (see chapter 4). We use the isSelected()/setSelected() or getState()/setState() methods to determine/set the selection state respectively. ActionListeners and ChangeListeners can be attached to a JRadioButtonMenuItem for notification about changes in its state (see JMenuItem discussion for inherited functionality). We often use JRadioButtonMenuItems in ButtonGroups to enforce the selection of only one item in a group at any given time.

 

UI Guideline : Widget overloading

As a general rule in UI Design, it is not desirable to overload components and use them for two purposes. By adding Checkboxes or Radio Buttons to a menu, you are changing the purpose of a menu from one of navigation to one of selection. This is an important point to understand.

Making this change is an acceptable design technique when it will speed operation and enhance usability by removing the need for a cumbersome dialog or option pane. However, it is important to assess that it does not otherwise adversely affect usability.

Groups of Radio Button or Checkbox menu items are probably best isolated by using a JSeparator.

 

12.1.10 The MenuElement interface

abstract interface javax.swing.MenuElement

This interface must be implemented by all components that wish to act as menu items. By implementing the methods of this interface any components can act as a menu item, making it quite easy to build our own.

The getSubElements() method returns an array of MenuElements containing the given item’s sub-elements. The processKeyEvent() and processMouseEvent() methods are called to process keyboard and mouse events respectively when the implementing component has the focus. Unlike methods with the same name in the java.awt.Component class, these two methods receive three parameters: the KeyEvent or MouseEvent, respectively, which should be processed, an array of MenuElements which forms the menu path to the implementing component, and the current MenuSelectionManager (see below). The menuSelectionChanged() method is called by the MenuSelectionManager when the implementing component is added or removed from its current selection state. The getComponent() method returns a reference to a component that is responsible for the rendering of the implementing component.

 

Note: The getComponent() method is interesting, as it allows classes that are not Components themselves to implement the MenuElement interface and act as menu elements when necessary. Such a class must provide a Component used for display in a menu, and this Component would be returned by getComponent(). This design has curious implications, allowing us to design robust JavaBeans that encapsulate an optional GUI representation. We can imagine a complex spell-checker or dictionary class imp! lementing the MenuElement interface and providing a custom component for display in a menu (a powerful and highly object-oriented bean indeed).

JMenuItem, JMenuBar, JPopupMenu, and JMenu all implement this interface. Note that each of their getComponent() methods simply return a this reference. Also note that by extending any of these implementing classes, we inherit MenuElement functionality and are therefore not required to implement it. (We won’t explicitly use this interface in any examples, as the custom component we will build at the end of this chapter is an extension of JMenu.)

12.1.11 MenuSelectionManager

class javax.swing.MenuSelectionManager

MenuSelectionManager is a service class responsible for managing menu selection throughout a single Java session. (Note that unlike most other service classes in Swing, MenuSelectionManager does not register its shared instance with AppContext--see chapter 2.) When MenuElement implementations receive MouseEvents or KeyEvents, these events should not be processed directly. Rather, they should be handed off to the MenuSelectionManager so that it may forward th! em to sub-components automatically. For instance, whenever a JMenuItem is activated by keyboard or mouse, or whenever a JMenuItem selection occurs, the menu item UI delegate is responsible for forwarding the corresponding event to the MenuSelectionManager if necessary. The following code shows how BasicMenuItemUI deals with mouse releases:

public void mouseReleased(MouseEvent e) {
      MenuSelectionManager manager = MenuSelectionManager.defaultManager();
      Point p = e.getPoint();
      if(p.x >= 0 && p.x < menuItem.getWidth() &&
       p.y >= 0 && p.y < menuItem.getHeight()) {
        manager.clearSelectedPath();
        menuItem.doClick(0);
      } 
      else {
        manager.processMouseEvent(e);
      }
}

The static defaultManager() method returns the MenuSelectionManager shared instance, and the clearSelectedPath() method tells the currently active menu hierarchy to close and unselect all menu components. In the code shown above, clearSelectedPath() will only be called if the mouse release occurs within the corresponding JMenuItem (in which case there is no need for the event to propogate any further). If this is not the case, the event is sent to MenuSelectionManager’s processMouseEvent() method which forwards it to other sub-components. JMenuItem doesn’t have any sub-components by default so not much interesting happens in this case. However, in the case of JMenu, which considers its popup menu a sub-component, sending a mouse released event to the MenuSelectionManager is expected no matter what (from BasicMenuUI):

public void mouseReleased(MouseEvent e) {
     MenuSelectionManager manager = MenuSelectionManager.defaultManager();
      manager.processMouseEvent(e);
      if (!e.isConsumed())
        manager.clearSelectedPath();
}

MenuSelectionManager will fire ChangeEvents whenever its setSelectedPath() method is called (i.e. each time a menu selection changes). As expected, we can attach ChangeListeners to listen for these events.

12.1.12 The MenuDragMouseListener interface

abstract interface javax.swing.event.MenuDragMouseListener

This listener receives notification when the mouse cursor enters, exits, is released, or is moved over a menu item.

12.1.13 MenuDragMouseEvent

class javax.swing.event.MenuDragMouseEvent

This event class is used to deliver information to MenuDragMouseListeners. It encapsulates the component source, event id, time of the event, bitwise or-masked int specifying which mouse button and/or keys (CTRL, SHIFT, ALT, or META) were pressed at the time of the event, x and y mouse coordinates, number of clicks immediately preceding the event, whether or not the event represents the platform-dependent popup trigger, an array of MenuElements leading to the source of the event, and the current MenuSelectionManager. This event inherits all MouseEvent functionali! ty (see API docs) and adds two methods for retrieving the array of MenuElements and the MenuSelectionManager.

12.1.14 The MenuKeyListener interface

abstract interface javax.swing.event.MenuKeyListener

This listener is notified when a menu item receives a key event corresponding to a key press, release, or type. These events don’t necessarily correspond to mnemonics or accelerators, and are received whenever a menu item is simply visible on the screen.

12.1.15 MenuKeyEvent

class javax.swing.event.MenuKeyEvent

This event class is used to deliver information to MenuKeyListeners. It encapsulates the component source, event id, time of the event, bitwise or-masked int specifying which mouse button and/or keys (CTRL, SHIFT, or ALT) were pressed at the time of the event, an int and char identifying the source key that caused the event, an array of MenuElements leading to the source of the event, and the current MenuSelectionManager. This event inherits all KeyEvent functionality (see API docs) and adds two methods for retrieving the array of MenuElements and the MenuSelectionManager.

12.1.16 The MenuListener interface

abstract interface javax.swing.event.MenuListener

This listener receives notification when a menu is selected, deselected, or cancelled. Three methods must be implemented by MenuListeners, and each takes a MouseEvent parameter: menuCanceled(), menuDeselected(), and menuSelected().

12.1.17 MenuEvent

class javax.swing.event.MenuEvent

This event class is used to deliver information to MenuListeners. It simply encapsulates a reference to its source Object.

12.1.18 The PopupMenuListener interface

abstract interface javax.swing.event.PopupMenuListener

This listener receives notification when a JPopupMenu is about to become visible, hidden, or when it is cancelled. Canceling a JPopupMenu also causes it to be hidden, so two PopupMenuEvents are fired in this case. A cancel occurs when the invoker component is resized or the window containing the invoker changes size or location. Three methods must be implemented by PopupMenuListeners, and each takes a PopupMenuEvent parameter: popupMenuCanceled(), pop! upMenuWillBecomeVisible(), and popupMenuWillBecomeInvisible().

12.1.19 PopupMenuEvent

class javax.swing.event.PopupMenuEvent

This event class is used to deliver information to PopupMenuListeners. It simply encapsulates a reference to its source Object.

12.1.20 JToolBar

class javax.swing.JToolBar

This class represents the Swing implementation of a toolbar. Toolbars are often placed directly below menu bars at the top of a frame or applet, and act as a container for any component (buttons and combo boxes are most common). The most convenient way to add buttons to a JToolBar is to use Actions (discussed below).

 

Note: Components often need their alignment setting tweaked to provide uniform positioning within JToolBar. This can be accomplished through use of the setAlignmentY() and setAlignmentX() methods. We will see that this is necessary in the final example of this chapter.

JToolBar also allows convenient addition of an inner JSeparator sub-class, JToolBar.Separator, to provide an empty space for visually grouping components. These separators can be added with either of the overloaded addSeparator() methods, one of which takes a Dimension parameter specifying the size of the separator.

Two orientations are supported, VERTICAL and HORIZONTAL, and managed by JToolBar’s orientation property. It uses a BoxLayout layout manager which is dynamically changed between Y_AXIS and X_AXIS when the orientation property changes.

JToolBar can be dragged in and out of its parent container if its floatable property is set to true. When dragged out of its parent, a JToolBar appears as a floating window and its border changes color depending on whether it can re-dock in its parent at a given location. If a JToolBar is dragged outside of its parent and released, it will be placed in its own JFrame which is fully maximizable, minimizable, and closable. When this frame is closed JToolBar will j! ump back into its most recent dock position in its original parent, and the floating JFrame will disappear. It is recommended that JToolBar is placed in one of the four sides of a container using a BorderLayout, leaving the other sides unused. This allows the JToolBar to be docked in any of that container’s side regions.

The protected createActionChangeListener() method is used when an Action (see below) is added to a JToolBar to create a PropertyChangeListener for internal use in responding to bound property changes that occur in that Action.

 

UI Guideline : 3 Uses for a Toolbar

Toolbars have become ubiquitious in modern software. However, they are often overused or misused and fail to achieve their objective of increased usability. There are three key uses which have subtle differences and implications.

Tool Selection or Mode Selection

Perhaps the most effective use of a toolbar is, as the name suggests, for the selection of a tool or operational mode. This is most common in drawing or image manipulation packages. The user selects the toolbar button to change the mode from "paintbrush" to "filler" to "draw box" to "cut" etc.. This is a highly effective use of toolbar, as the small icons are usually sufficient to render a suitable tool image. Many images for this purpose have been adopted as a defacto standard. If you are developing such a tool selection toolbar, it would be advisable to stick closely to icons which have been used by similar existing products.

Functional Selection

The earliest use of toolbar was to replace the selection of a specific function from the menu. This led to them being called "speedbars" or "menubars". The idea was that the small icon button was faster and easier to acquire than the menu selection and that usability was enhanced as a result. This worked well for many common functions in file oriented applications, such as "open file", "new file", "save", "cut", "copy" and "paste". In fact, most of us would recognise the small icons for all of these functions. However, with other more application specific functions, it has been much harder for icon designers to come up with appropriate designs. This often leads to applications which have a confusing and intimidating array of icons across the top of the screen. This can detract from usability. As a general rule of thumb, stick to common cross application functions when overloading menu selections with toolbar buttons. If you do need to break the rule th! en consider selecting annotated buttons for the toolbar.

Navigational Selection

The 3rd use for toolbars has been for navigational selection. This often means replacing or overloading menu options from a "Window", "Go" or "Form" menu. These menu options are used to select a specific screen to come to the front. The toolbar buttons replace or overload the menu option and allow the navigational selection to be made supposedly by faster means. However, again this suffers from the problem of appropriate icon design. It is usually too difficult to devise a suitable set of icons which have clear and unambiguous meaning. Therefore, as a rule of thumb, consider the use of annotated buttons on the toolbar.

 

12.1.21 Custom JToolBar separators

Unfortunately Swing does not include a toolbar-specific separator component that will display a vertical or horizontal line depending on current toolbar orientation. The following psuedo-code shows how we can build such a component under the assumption that it will always have a JToolBar as direct parent:

public class MyToolBarSeparator extends JComponent 
{
    public void paintComponent(Graphics g) {
      super.paintComponent(g);
      if (getParent() instanceof JToolBar) {
        if (((JToolBar) getParent()).getOrientation() == JToolBar.HORIZONTAL) {
          // paint a vertical line
        } 
        else {
          // paint a horizontal line
        }
      }
    }

    public Dimension getPreferredSize() {
      if (getParent() instanceof JToolBar) {
        if (((JToolBar) getParent()).getOrientation() == JToolBar.HORIZONTAL) {
          // return size of vertical bar
        } 
        else {
          // return size of horizontal bar
        }
      }
    }
}

UI Guideline : Use of a separator

The failure to include a separator for toolbars really was an oversight by the Swing designers. Again, use the separator to group related functions or tools. For example, if the function all belong on the same menu then group them together, or if the tools (or modes) are related such as "cut", "copy", "paste" then group them together and separate them from others with a separator.

Visual grouping like this improves visual separation by introducing a visual layer. The viewer can first acquire a group of buttons and then a specific button. They will also learn with directional memory the approximate position of each group. By separating them you will improve the usability by helping them to acquire the target better when using the mouse.

 

12.1.22 Changing JToolBar’s floating frame behavior

The behavior of JToolBar’s floating JFrame is certainly useful, but it is arguable whether the maximization and resizability should be allowed. Though we cannot control whether or not a JFrame can be maximized, we can control whether or not it can be resized. To enforce non-resizability in JToolBar’s floating JFrame (and set its dispayed title while we’re at it) we need to override its UI delegate and customize the createFloatingFrame() method as follows:

public class MyToolBarUI extends javax.swing.plaf.metal.MetalToolBarUI {

    protected JFrame createFloatingFrame(JToolBar toolbar) {
      JFrame frame = new JFrame(toolbar.getName());
      frame.setTitle("My toolbar");
      frame.setResizable(false);
      WindowListener wl = createFrameListener();
      frame.addWindowListener(wl);
      return frame;
    }
}

To assign MyToolBarUI as a JToolBar’s UI delegate we can do the following:

mytoolbar.setUI(new MyToolBarUI());

To force use of this delegate on a global basis we can do the following before any JToolBars are instantiated:

UIManager.getDefaults().put(

"ToolBarUI","com.mycompany.MyToolBarUI");

Note that we may also have to add an associated Class instance to the UIDefaults table for this to work (see chapter 21).

 

UI Guideline : Use of a Floating Frame

It is probably best to restrict the use of a floating toolbar frame to toolbars being used for tool or mode selection (see guideline in 12.1.20).

 

12.1.23 The Action interface

abstract interface javax.swing.Action

This interface describes a helper object which extends ActionListener and which supports a set of bound properties. We use appropriate add() methods in the JMenu, JPopupMenu, and JToolBar classes to add an Action which will use information from the given instance to create and return a component that is appropriate for that container (a JMenuItem in the case of the first two, a JButton in the case of the latter). The same! Action instance can be used to create an arbitrary number of menu items or toolbar buttons.

Because Action extends ActionListener, the actionPerformed() method is inherited and can be used to encapsulte appropriate ActionEvent handling code. When a menu item or toolbar button is created using an Action, the resulting component is registered as a PropertyChangeListener with the Action, and the Action is registered as an ActionListener with the component. Thus, whenever a change occurs to one of that Action’s bound properties, all components with registered PropertyChangeListeners will receive notification. This provides a convenient means for allowing identical functionality in menus, toolbars, and popup menus with minimum code repetition and object creation.

The putValue() and getValue() methods are intended to work with a Hashtable-like structure to maintain an Action’s bound properties. Whenever the value of a property changes, we are expected to fire PropertyChangeEvents to all registered listeners. As expected, methods to add and remove PropertyChangeListeners are provided.

The Action interface defines five static property keys intended for use by JMenuItems and JButtons created with an Action instance:

String DEFAULT: [not used]

String LONG_DESCRIPTION: Used for a lengthy description of an Action.

String NAME: Used as the text in associated and displayed in JMenuItems and JButtons.

String SHORT_DESCRIPTION: Used for the tooltip text of associated JMenuItems and JButtons.

String SMALL_ICON: Used as the icon in associated JMenuItems and JButtons.

12.1.24 AbstractAction

class javax.swing.AbstractAction

This class is an abstract implementation of the Action interface. Along with the properties inherited from Action, AbstractAction defines the enabled property which provides a means of enabling/disabling all associated components registered as PropertyChangeListeners. A SwingPropertyChangeSupport instance is used to manage the firing of PropertyChangeEvents to all registered PropertyChan! geListeners (see chapter 2 for more about SwingPropertyChangeSupport).

 

12.2 Basic text editor: part I - menus

In this example we begin the construction of a basic text editor application using a menu bar and several menu items. The menu bar contains two JMenus labeled "File" and "Font." The "File" menu contains JMenuItems for creating a new (empty) document, opening a text file, saving the current document as a text file, and exiting the application. The "Font" menu contains JCheckBoxMenuItems for making the document bold and/or italic, as well as JRadioButtonMenuItems organized into a ButtonGroup allowing selection of a single fon! t.

Figure 12.1 JMenu containing JMenuItems with mnemonics and icons.

<<file figure12-1.gif>>

Figure 12.2 JMenu containing JRadioButtonMenuItems and JCheckBoxMenuItems.

<<file figure12-2.gif>>

The Code: BasicTextEditor.java

see \Chapter12\1

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

import javax.swing.*;
import javax.swing.event.*;

public class BasicTextEditor extends JFrame 
{
	public static final String FONTS[] = { "Serif", "SansSerif", 
		"Courier" };
	protected Font m_fonts[];

	protected JTextArea m_monitor;
	protected JMenuItem[] m_fontMenus;
	protected JCheckBoxMenuItem m_bold;
	protected JCheckBoxMenuItem m_italic;

	protected JFileChooser m_chooser;

	public BasicTextEditor()
	{
		super("Basic text editor: part I - Menus");
		setSize(450, 350);

		m_fonts = new Font[FONTS.length];
		for (int k = 0; k < FONTS.length; k++)
		   m_fonts[k] = new Font(FONTS[k], Font.PLAIN, 12);

		m_monitor = new JTextArea();
		JScrollPane ps = new JScrollPane(m_monitor);
		getContentPane().add(ps, BorderLayout.CENTER);

		m_monitor.append("Basic text editor");

		JMenuBar menuBar = createMenuBar();
		setJMenuBar(menuBar);

		m_chooser = new JFileChooser(); 
		m_chooser.setCurrentDirectory(new File("."));

		WindowListener wndCloser = new WindowAdapter()
		{
			public void windowClosing(WindowEvent e) 
			{
				System.exit(0);
			}
		};
		addWindowListener(wndCloser);
		
		updateMonitor();
		setVisible(true);
	}

	protected JMenuBar createMenuBar()
	{
		final JMenuBar menuBar = new JMenuBar();
		
		JMenu mFile = new JMenu("File");
		mFile.setMnemonic('f');

		JMenuItem item = new JMenuItem("New");
		item.setIcon(new ImageIcon("file_new.gif"));
		item.setMnemonic('n');
		ActionListener lst = new ActionListener() 
		{ 
			public void actionPerformed(ActionEvent e)
			{
				m_monitor.setText("");
			}
		};
		item.addActionListener(lst);
		mFile.add(item);

		item = new JMenuItem("Open...");
		item.setIcon(new ImageIcon("file_open.gif"));
		item.setMnemonic('o');
		lst = new ActionListener() 
		{ 
			public void actionPerformed(ActionEvent e)
			{
                                BasicTextEditor.this.repaint();
				if (m_chooser.showOpenDialog(BasicTextEditor.this) != 
					JFileChooser.APPROVE_OPTION)
					return;
                                Thread runner = new Thread() {
                                  public void run() {
				    File fChoosen = m_chooser.getSelectedFile();
				    try
				    {
					FileReader in = new FileReader(fChoosen);
					m_monitor.read(in, null);
					in.close();
				    } 
				    catch (IOException ex) 
				    {
					ex.printStackTrace();
				    }
                                  }
                                };
                                runner.start();
			}
		};
		item.addActionListener(lst);
		mFile.add(item);

		item = new JMenuItem("Save...");
		item.setIcon(new ImageIcon("file_save.gif"));
		item.setMnemonic('s');
		lst = new ActionListener() 
		{ 
			public void actionPerformed(ActionEvent e)
			{
                                BasicTextEditor.this.repaint();
				if (m_chooser.showSaveDialog(BasicTextEditor.this) != 
					JFileChooser.APPROVE_OPTION)
					return;
                                Thread runner = new Thread() {
                                  public void run() {
			 	    File fChoosen = m_chooser.getSelectedFile();
				    try
				    {
					FileWriter out = new FileWriter(fChoosen);
					m_monitor.write(out);
					out.close();
				    } 
				    catch (IOException ex) 
				    {
					ex.printStackTrace();
				    }
                                  }
                                };
                                runner.start();
			}
		};
		item.addActionListener(lst);
		mFile.add(item);

		mFile.addSeparator();

		item = new JMenuItem("Exit");
		item.setMnemonic('x');
		lst = new ActionListener() 
		{ 
			public void actionPerformed(ActionEvent e)
			{
				System.exit(0);
			}
		};
		item.addActionListener(lst);
		mFile.add(item);
		menuBar.add(mFile);

		ActionListener fontListener = new ActionListener() 
		{ 
			public void actionPerformed(ActionEvent e)
			{
				updateMonitor();
			}
		};
		
		JMenu mFont = new JMenu("Font");
		mFont.setMnemonic('o');

		ButtonGroup group = new ButtonGroup();
		m_fontMenus = new JMenuItem[FONTS.length];
		for (int k = 0; k < FONTS.length; k++)
		{
			int m = k+1;
			m_fontMenus[k] = new JRadioButtonMenuItem(
				m+" "+FONTS[k]);
			boolean selected = (k == 0);
			m_fontMenus[k].setSelected(selected);
			m_fontMenus[k].setMnemonic('1'+k);
			m_fontMenus[k].setFont(m_fonts[k]);
			m_fontMenus[k].addActionListener(fontListener);
			group.add(m_fontMenus[k]);
			mFont.add(m_fontMenus[k]);
		}
		
		mFont.addSeparator();

		m_bold = new JCheckBoxMenuItem("Bold");
		m_bold.setMnemonic('b');
		Font fn = m_fonts[1].deriveFont(Font.BOLD);
		m_bold.setFont(fn);
		m_bold.setSelected(false);
		m_bold.addActionListener(fontListener);
		mFont.add(m_bold);

		m_italic = new JCheckBoxMenuItem("Italic");
		m_italic.setMnemonic('i');
		fn = m_fonts[1].deriveFont(Font.ITALIC);
		m_italic.setFont(fn);
		m_italic.setSelected(false);
		m_italic.addActionListener(fontListener);
		mFont.add(m_italic);

		menuBar.add(mFont);

		return menuBar;
	}

	protected void updateMonitor()
	{
		int index = -1;
		for (int k = 0; k < m_fontMenus.length; k++)
		{
			if (m_fontMenus[k].isSelected())
			{
				index = k;
				break;
			}
		}
		if (index == -1)
			return;

		if (index==2)  // Courier
		{
			m_bold.setSelected(false);
			m_bold.setEnabled(false);
			m_italic.setSelected(false);
			m_italic.setEnabled(false);
		}
		else
		{
			m_bold.setEnabled(true);
			m_italic.setEnabled(true);
		}

		int style = Font.PLAIN;
		if (m_bold.isSelected())
			style |= Font.BOLD;
		if (m_italic.isSelected())
			style |= Font.ITALIC;
		Font fn = m_fonts[index].deriveFont(style);
		m_monitor.setFont(fn);
		m_monitor.repaint();
	}

	public static void main(String argv[]) 
	{
		new BasicTextEditor();
	}
}

Understanding the Code

Class BasicTextEditor

This class extends JFrame and provides the parent frame for our example. One class variable is declared:

String FONTS[]: an array of font family names.

Instance variables:

Font[] m_fonts: an array of Font instances which can be used to render our JTextArea editor.

JTextArea m_monitor: used as our text editor.

JMenuItem[] m_fontMenus: an array of menu items representing available fonts.

JCheckBoxMenuItem m_bold: menu item which sets/unsets the bold property of the current font.

JCheckBoxMenuItem m_italic: menu item which sets/unsets the italic property of the current font.

JFileChooser m_chooser: used to load and save simple text files.

The Menu1 constructor populates our m_fonts array with Font instances corresponding to the names provided in FONTS[]. The m_monitor JTextArea is then created and placed in a JScrollPane. This scroll pane is added to the center of our frame’s content pane and we append some simple text to m_monitor for display at startup. Our createMenuBar() method is called to create the menu bar to manage t! his application, and this menu bar is then added to our frame using the setJMenuBar() method.

The createMenuBar() method creates and returns a JMenuBar. Each menu item receives an ActionListener to handle it's selection. Two menus are added titled "File" and "Font." The "New" menu item in the "File" menu is responsible for creating a new (empty) document. It doesn’t really replace JTextArea’s Document. Instead it simply clears the contents of our editor component. Note that an icon is used in this menu item. Also note that this menu item can be selected with the keyboard by pressing ‘n’ when the "File" menu’s popup is visible, because we assigne! d it ‘n’ as a mnemonic. The File menu was also assigned a mnemonic character, ‘f,’ and pressing ALT-F while the application frame is active, the "File" popup will be displayed allowing navigation with either the mouse or keyboard (all other menus and menu items in this example also receive appropriate mnemonics).

 

Bug Alert! Even though ALT-F will get processed by the corresponding menu in the menu bar, the key event that is generated is still received by JTextArea. Thus, you will often see an ‘f’ added to the text area even though ALT was held down when ‘f’ was pressed. It is possible to work around this problem by extending JTextArea (or any other JTextComponent) as follows:

 

public class MyTextArea extends JTextArea
{
  protected synchronized void processComponentKeyEvent(
   KeyEvent anEvent) {
    super.processComponentKeyEvent(anEvent);
    ivLastKeyEventWasAlt = anEvent.isAltDown();
  }

  protected synchronized void processInputMethodEvent(
   InputMethodEvent e) {
    if (ivLastKeyEventWasAlt) {
      e.consume();
    }
    super.processInputMethodEvent(e);
  }
  private transient boolean ivLastKeyEventWasAlt = false;
}

 

Note that this workaround was orginally posted to the JDC Bug Parade and it should only be considered a temporary solution until the problem is actually fixed.

The "Open" menu item brings up our m_chooser JFileChooser component (discussed in chapter 14) to allow selection of a text file to open. Once a text file is selected, we open a FileReader on it and invoke read() on our JTextArea component to read the file's content (which creates a new PlainDocument containing the selected file’s content to replace the current JTextArea document--see chapter 11). The "Save" menu item brings up m_chooser to select a destination and file name to save the current text to. Once a text file is selected, we open a FileWriter on it and invoke write() on our JTextArea component to write its content to the destination file. Both of these I/O operations are wrapped in separate threads to avoid clogging up the event dispatching thread.

 

Bug Alert! We have added code to repaint the whole application before the I/O operations occur. This is because when a Swing menu is hidden by a dialog (in this case a JFileChooser dialog) that pops up immediately in response to a menu selection, the menu will often not perform all necessary repainting of itself and components that lie below its popup. To make sure that no remnants of menu rendering are left around we force a repaint of the whole frame before moving on.

The "Exit" menu item terminates program execution. It is separated from the first three menu items with a menu separator to create a more logical display.

The "Font" menu consists of several menu items used to select the font and font style used in our editor. All of these items receive the same ActionListener which invokes our updateMonitor() method (see below). To give the user an idea of how each font looks, each font is used to render the corresponding menu item text. Since only one font can be selected at any given time, we use JRadioButtonMenuItems for these menu items, and add them all to a ButtonGroup instance which manages a single selection.

To create each menu item we iterate through our FONTS array and create a JRadioButtonMenuItem corresponding to each entry. Each item is set to unselected (except for the first one), assigned a numerical mnemonic corresponding to the current FONTS array index, assigned the appropriate Font instance for rendering its text, assigned our multi-purpose ActionListener, and added to our ButtonGroup along with the others.

The two other menu items in the "Font" menu manage the bold and italic font properties. They are implemented as JCheckBoxMenuItems since these properties can be selected or unselected independently. These items also are assigned the same ActionListener as the radio button items to process changes in their selected state.

The updateMonitor() method updates the current font used to render the editing component by checking the state of each check box item and determining which radio button item is currently selected. The m_bold and m_italic components are disabled and unselected if the Courier font is selected, and enabled otherwise. The appropriate m_fonts array element is selected and a Font instance is derived from it corresponding to the current state of the check box items using Font’s deriveFont() method (see chapter 2).

 

Note: Surprisingly the ButtonGroup class does not provide a direct way to determine which component is currently selected. So we have to examine m_fontMenus array elements in turn to determine the selected font index. Alternatively we could save the font index in an enhanced version of our ActionListener.

Running the Code

Open a text file, make some changes, and save it as a new file. Change the font options and note how the text area is updated. Select the Courier font and note how it disables the bold and italic check box items (it also unchecks them if they were previously checked). Select another font and note how this re-enables check box items. Figure 12.1 shows BasicTextEditor’s "File" menu, and figure 12.2 shows the "Font" menu. Note how the mnemonics are underlined and the images appear to the right of the text by default (just like buttons).

 

UI Guideline : File oriented applications

This is an example of a menu being used on a file oriented application. Menus were first developed to be used in this fashion. The inclusion of a menu on such an application is essential, as users have come to expect one. There are clearly defined platform standards for the layout of such a menu and it is best that you ahere to these. Almost always the "File" menu comes first (to the LHS).

Note also the use of the elipsis "..." on the "Open..." and "Save..." options. This is a standard technique which gives a visual affordance to the behaviour that a dialog will open when the menu item is selected.

 

UI Guideline : Correct use of separator and widget overloading This example shows clearly how a menu in a simple application can have selection controls added to speed operation and ease usability. The separator is used to group and separate the selection of the font type from the font style.

 

12.3 Basic text editor: part II - toolbars and actions

Swing provides the Action interface to simplify the creation of menu items. As we know, implementations of this interface encapsulate both knowledge of what to do when a menu item or toolbar button is selected (by extending the ActionListener interface) and knowledge of how to render the component itself (by holding a collection of bound properties such as NAME, SMALL_ICON, etc.). We can create both a menu item and a toolbar button from a single Action instance, conserving code and providing a reliable means of ensuring consistency between menus and toolbars.

The following example uses the AbstractAction class to add a toolbar to our BasicTextEditor application. By converting the ActionListeners used in the example above to AbstractActions, we can use these actions to create both toolbar buttons and menu items with very little additional work.

Figure 12.3 Process of un-docking, dragging, and docking a floating JToolBar.

<<file figure12-3.gif>>

Figure 12.4 A floating JToolBar placed in a non-dockable region.

<<file figure12-4.gif>>

The Code: BasicTextEditor.java

see \Chapter12\2

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

import javax.swing.*;
import javax.swing.event.*;

public class BasicTextEditor extends JFrame 
{
  // Unchanged code from section 12.2

  protected JToolBar m_toolBar;

  protected JMenuBar createMenuBar() {
    final JMenuBar menuBar = new JMenuBar();
    JMenu mFile = new JMenu("File");
    mFile.setMnemonic('f');

    ImageIcon iconNew = new ImageIcon("file_new.gif");
    Action actionNew = new AbstractAction("New", iconNew) { 
      public void actionPerformed(ActionEvent e) {
        m_monitor.setText("");
      }
    };

    JMenuItem item =  mFile.add(actionNew);  
    item.setMnemonic('n');

    ImageIcon iconOpen = new ImageIcon("file_open.gif");
    Action actionOpen = new AbstractAction("Open...", iconOpen) { 
      public void actionPerformed(ActionEvent e) {
        // Unchanged code from section 12.2
      }
    };

    item =  mFile.add(actionOpen);  
    item.setMnemonic('o');

    ImageIcon iconSave = new ImageIcon("file_save.gif");
    Action actionSave = new AbstractAction("Save...", iconSave) { 
      public void actionPerformed(ActionEvent e) {
        // Unchanged code from section 12.2
      }
    };

    item =  mFile.add(actionSave);  
    item.setMnemonic('s');

    mFile.addSeparator();

    Action actionExit = new AbstractAction("Exit") { 
      public void actionPerformed(ActionEvent e) {
        System.exit(0);
      }
    };

    item =  mFile.add(actionExit);  
    item.setMnemonic('x');
    menuBar.add(mFile);

    m_toolBar = new JToolBar();
    JButton btn1 = m_toolBar.add(actionNew);
    btn1.setToolTipText("New text");
    JButton btn2 = m_toolBar.add(actionOpen);
    btn2.setToolTipText("Open text file");
    JButton btn3 = m_toolBar.add(actionSave);
    btn3.setToolTipText("Save text file");

    // Unchanged code from section 12.2

    getContentPane().add(m_toolBar, BorderLayout.NORTH);

    return menuBar;
  }

  // Unchanged code from section 12.2
}

Understanding the Code

Class BasicTextEditor

This class now declares one more instance variable, JToolBar m_toolBar. The constructor remains unchanged and is not listed here. The createMenuBar() method now creates AbstractAction instances instead of ActionListeners. These objects encapsulate the same action handling code we defined in the last example, as well as the text and icon to display in associated menu items and toolbar buttons. This allows us to create JMenuItems using the JMenu.add(Action a) method, and JButtons using the JToolBar.add(Action a) method. These methods return instances that we can treat as any other button component and do things such as set the background color assign a different text alignment.

Our JToolBar component is placed in the NORTH region of our content pane, and we make sure to leave the EAST, WEST, and SOUTH regions empty allowing it to dock on all sides.

Running the Code

Verify that the toolbar buttons work as expected by opening and saving a text file. Try dragging the toolbar from its ‘handle’ and note how it is represented by an empty gray window as it is dragged. The border will change to a dark color when the window is in a location where it will dock if the mouse is released. If the border does not appear dark, releasing the mouse will result in the toolbar being placed in its own JFrame. Figure 12.3 illustrates the simple process of undocking, dragging, and docking our toolbar in a new location. Figure 12.4 shows our toolbar in its own JFrame when undocked and released outside of a dockable region (also referred to as a hotspot).

 

Note: The current JToolBar implementation does not easily allow the use of multiple floating toolbars as is common in many modern applications. We hope to see more of this functionality built into future versions of Swing.

 

UI Guideline : Vertical or Horizontal?

In some applications, you may prefer to leave the selection of a vertical or horizontal toolbar to the user. More often than not, you as designer can make that choice for them. Consider whether vertical or horizontal space is more valuable for what you need to display. If, for example, you are displaying Letter text then you probably need vertical space more than horizontal space. Usually in PC applications, vertical space is at a premium.

When vertical space is at a premium, place the toolbar vertically thus freeing up valuable vertical space.
When horizontal space is at a premium, place the toolbar horizontally thus freeing up valuable horizontal space.

Almost never allow a floating toolbar, as it has a tendency to get lost under other windows. Floating toolbars are for advanced users who understand the full operation of the computer system. Consider the technical level of your user group before making the design choice for a floating toolbar.

 

12.4 Basic text editor: part III - custom toolbar components

Using Actions to create toolbar buttons is easy, but not always desirable if we wish to have complete control over our toolbar components. In this section we build off of BasicTextEditor and place a JComboBox in the toolbar allowing Font selection. We also use instances of our own custom buttons, SmallButton and SmallToggleButton, in the toolbar. Both of these button classes use different borders to signify different states. SmallButton uses a raised border when the mouse passes over it, no border when the mouse is not within its bounds, and a lowered border when a mouse press occurs. SmallToggleButton uses a raised border when unselected and a lowered border when selected.

Figure 12.5 JToolBar with custom buttons and a JComboBox.

<<file figure12-5.gif>>

The Code: BasicTextEditor.java

see \Chapter12\3

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

import javax.swing.*;
import javax.swing.event.*;

public class BasicTextEditor extends JFrame 
{
  // Unchanged code from section 12.3

  protected JComboBox m_cbFonts;
  protected SmallToggleButton m_bBold;
  protected SmallToggleButton m_bItalic;

  // Unchanged code from section 12.3

  protected JMenuBar createMenuBar()
  {
    // Unchanged code from section 12.3

    m_toolBar = new JToolBar();
    JButton bNew = new SmallButton(actionNew, "New text");
    m_toolBar.add(bNew);

    JButton bOpen = new SmallButton(actionOpen, "Open text file");
    m_toolBar.add(bOpen);
        
    JButton bSave = new SmallButton(actionSave, "Save text file");
    m_toolBar.add(bSave);
        
    JMenu mFont = new JMenu("Font");
    mFont.setMnemonic('o');

    // Unchanged code from section 12.3

    mFont.addSeparator();

    m_toolBar.addSeparator();
    m_cbFonts = new JComboBox(FONTS);
    m_cbFonts.setMaximumSize(m_cbFonts.getPreferredSize());
    m_cbFonts.setToolTipText("Available fonts");
    ActionListener lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        int index = m_cbFonts.getSelectedIndex();
        if (index < 0)
          return;
        m_fontMenus[index].setSelected(true);
        updateMonitor();
      }
    };
    m_cbFonts.addActionListener(lst);
    m_toolBar.add(m_cbFonts);

    m_bold = new JCheckBoxMenuItem("Bold");
    m_bold.setMnemonic('b');
    Font fn = m_fonts[1].deriveFont(Font.BOLD);
    m_bold.setFont(fn);
    m_bold.setSelected(false);
    m_bold.addActionListener(fontListener);
    mFont.add(m_bold);

    m_italic = new JCheckBoxMenuItem("Italic");
    m_italic.setMnemonic('i');
    fn = m_fonts[1].deriveFont(Font.ITALIC);
    m_italic.setFont(fn);
    m_italic.setSelected(false);
    m_italic.addActionListener(fontListener);
    mFont.add(m_italic);

    menuBar.add(mFont);

    m_toolBar.addSeparator();

    ImageIcon img1 = new ImageIcon("font_bold1.gif");
    ImageIcon img2 = new ImageIcon("font_bold2.gif");
    m_bBold = new SmallToggleButton(false, img1, img2, "Bold font");
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        m_bold.setSelected(m_bBold.isSelected());
        updateMonitor();
      }
    };
    m_bBold.addActionListener(lst);
    m_toolBar.add(m_bBold);
        
    img1 = new ImageIcon("font_italic1.gif");
    img2 = new ImageIcon("font_italic2.gif");
    m_bItalic = new SmallToggleButton(false, img1, img2, "Italic font");
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        m_italic.setSelected(m_bItalic.isSelected());
        updateMonitor();
      }
    };
    m_bItalic.addActionListener(lst);
    m_toolBar.add(m_bItalic);

    getContentPane().add(m_toolBar, BorderLayout.NORTH);
    return menuBar;
  }

  protected void updateMonitor() {
    int index = -1;
    for (int k=0; k<m_fontMenus.length; k++) {
      if (m_fontMenus[k].isSelected()) {
        index = k;
        break;
      }
    }
    if (index == -1)
      return;
    boolean isBold = m_bold.isSelected();
    boolean isItalic = m_italic.isSelected();

    m_cbFonts.setSelectedIndex(index);

    if (index==2) {   //Courier
      m_bold.setSelected(false);
      m_bold.setEnabled(false);
      m_italic.setSelected(false);
      m_italic.setEnabled(false);
      m_bBold.setSelected(false);
      m_bBold.setEnabled(false);
      m_bItalic.setSelected(false);
      m_bItalic.setEnabled(false);
    }
    else {
      m_bold.setEnabled(true);
      m_italic.setEnabled(true);
      m_bBold.setEnabled(true);
      m_bItalic.setEnabled(true);
    }

    if (m_bBold.isSelected() != isBold)
      m_bBold.setSelected(isBold);
    if (m_bItalic.isSelected() != isItalic)
      m_bItalic.setSelected(isItalic);

    int style = Font.PLAIN;
    if (isBold)
      style |= Font.BOLD;
    if (isItalic)
      style |= Font.ITALIC;
    Font fn = m_fonts[index].deriveFont(style);
    m_monitor.setFont(fn);
    m_monitor.repaint();
  }

  public static void main(String argv[]) { 
    new BasicTextEditor(); 
  }
}

class SmallButton extends JButton implements MouseListener
{
  protected Border m_raised;
  protected Border m_lowered;
  protected Border m_inactive;

  public SmallButton(Action act, String tip) {
    super((Icon)act.getValue(Action.SMALL_ICON));
    m_raised = new BevelBorder(BevelBorder.RAISED);
    m_lowered = new BevelBorder(BevelBorder.LOWERED);
    m_inactive = new EmptyBorder(2, 2, 2, 2);
    setBorder(m_inactive);
    setMargin(new Insets(1,1,1,1));
    setToolTipText(tip);
    addActionListener(act);
    addMouseListener(this);
    setRequestFocusEnabled(false);
  }

  public float getAlignmentY() { return 0.5f; }

  public void mousePressed(MouseEvent e) { 
    setBorder(m_lowered);
  }
  public void mouseReleased(MouseEvent e) {
    setBorder(m_inactive);
  }
  public void mouseClicked(MouseEvent e) {}
  public void mouseEntered(MouseEvent e) {
    setBorder(m_raised);
  }
  public void mouseExited(MouseEvent e) {
    setBorder(m_inactive);
  }
}

class SmallToggleButton extends JToggleButton implements ItemListener 
{
  protected Border m_raised;
  protected Border m_lowered;

  public SmallToggleButton(boolean selected, 
   ImageIcon imgUnselected, ImageIcon imgSelected, String tip) {
    super(imgUnselected, selected);
    setHorizontalAlignment(CENTER);
    setBorderPainted(true);
    m_raised = new BevelBorder(BevelBorder.RAISED);
    m_lowered = new BevelBorder(BevelBorder.LOWERED);
    setBorder(selected ? m_lowered : m_raised);
    setMargin(new Insets(1,1,1,1));
    setToolTipText(tip);
    setRequestFocusEnabled(false);
    setSelectedIcon(imgSelected);
    addItemListener(this);
  }

  public float getAlignmentY() { return 0.5f; }

  public void itemStateChanged(ItemEvent e) {
    setBorder(isSelected() ? m_lowered : m_raised);
  }
}

Understanding the Code

Class BasicTextEditor

BasicTextEditor now declares three new instance variables:

JComboBox m_cbFonts: combo box containing available font names.

SmallToggleButton m_bBold: custom toggle button representing the bold font style.

SmallToggleButton m_bItalic: custom toggle button representing the italic font style.

The createMenuBar() method now creates three instances of the SmallButton class (see below) corresponding to our pre-existing "New," "Open," and "Save" toolbar buttons. These are constructed by passing the appropriate Action (which we built in part II) as well as a tooltip String to the SmallButton constructor. Then we create a combo box with all available font names and add it to the toolbar. The setMaximumSize() method is called on the combo box to reduce its size to a necessary maximum (otherwise it ! will fill all unoccupied space in our toolbar). An ActionListener is then added to monitor combo box selection. This listener selects the corresponding font menu item because the combo box and font radio button menu items must always be in synch. It then calls our updateMonitor() method.

Two SmallToggleButtons are created and added to our toolbar to manage the bold and italic font properties. Each button receives an ActionListener which selects/deselects the corresponding menu item (because both the menu items and toolbar buttons must remain in synch) and calls our updateMonitor() method.

Our updateMonitor() method receives some additional code to provide consistency between our menu items and toolbar controls. This method relies on the state of the menu items, which is why the toolbar components first set the corresponding menu items when selected. The code added here is self-explanatory and just involves enabling/disabling and selecting/deselecting components to preserve consistency.

Class SmallButton

SmallButton represents a small push button to be used in a toolbar. It implements the MouseListener interface to process mouse input. Three instance variables are declared:

Border m_raised: border to be used when the mouse cursor is located over the button.

Border m_lowered: border to be used when the button is pressed.

Border m_inactive: border to be used when the mouse cursor is located outside the button.

The SmallButton constructor takes an Action parameter, which is added as an ActionListener and performs an appropriate action when the button is pressed, and a String representing the tooltip text. Several familiar properties are assigned and the icon encapsulated within the Action is used for this buttons icon. SmallButton also adds itself as a MouseListener and sets its tooltip text to the given String passed to the constructor. Note that the requestFocusEnabled property is set to false so that when this button is clicked focus will not be transferred out of our JTextArea editor component.

The getAlignmentY() method is overriden to return a constant value of 0.5f, indicating that this button should always be placed in the middle of the toolbar in the vertical direction. The remainder of SmallButton represents an implementation of the MouseListener interface which sets the border based on mouse events. The border is set to m_inactive when the is mouse located outside its bounds, m_active when the mouse is located inside its bounds, and m_lowered when the button is pressed.

Class SmallToggleButton

SmallToggleButton extends JToggleButton and implements the ItemListener interface to process changes in the button's selection state. Two instance variables are declared:

Border m_raised: border to be used when the button is unselected (unchecked).

Border m_lowered: border to be used when the button is selected (checked).

The SmallToggleButton constructor takes four arguments:

boolean selected: initial selection state.

ImageIcon imgUnselected: icon for use when unselected.

ImageIcon imgSelected: icon for use when selected.

String tip: tooltip message.

In the constructor, several familiar button properties are set, and a raised or lowered border is assigned depending on the initial selection state. Each instance is added to itself as an ItemListener to receive notification about changes in its selection. Thus the itemStateChanged() method is implemented which simply sets the button's border accordingly corresponding to the new selected state.

Running the Code

Verify that the toolbar components (combobox and toggle buttons) change the editor's font as expected. Check which menu and toolbar components work consistently (menu item selections result in changes in the toolbar controls, and vice versa).

 

UI Guideline : Tooltip Help Tooltip Help on mouse-over is a "must have" technical addition for small toolbar buttons. The relatively recent innovation of tooltips has greatly improved the usability of toolbars. Don't get caught delivering a toolbar without one, but make sure that your tooltip text is meaningful to the user!

 

12.5 Basic text editor: part IV - custom menu components

In this section we will show how to build a custom menu component, ColorMenu, which allows selection of a color from a grid of small colored panes (which are instances of the inner class ColorMenu.ColorPane). By extending JMenu we inherit all MenuElement functionality (see 12.1.10), making custom menu creation quite easy.

Figure 12.6 Custom menu component used for quick color selection.

<<file figurer12-6.gif>>

The Code: BasicTextEditor.java

see \Chapter12\4

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;

public class BasicTextEditor extends JFrame 
{
  // Unchanged code from section 12.4

  protected JMenuBar createMenuBar()
  {
    // Unchanged code

    JMenu mOpt = new JMenu("Options");
    mOpt.setMnemonic('p');

    ColorMenu cm = new ColorMenu("Foreground");
    cm.setColor(m_monitor.getForeground());
    cm.setMnemonic('f');
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        ColorMenu m = (ColorMenu)e.getSource();
        m_monitor.setForeground(m.getColor());
      }
    };
    cm.addActionListener(lst);
    mOpt.add(cm);

    cm = new ColorMenu("Background");
    cm.setColor(m_monitor.getBackground());
    cm.setMnemonic('b');
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        ColorMenu m = (ColorMenu)e.getSource();
        m_monitor.setBackground(m.getColor());
      }
    };
    cm.addActionListener(lst);
    mOpt.add(cm);
    menuBar.add(mOpt);

    getContentPane().add(m_toolBar, BorderLayout.NORTH);
    return menuBar;
  }

  // Unchanged code
}

class ColorMenu extends JMenu
{
  protected Border m_unselectedBorder;
  protected Border m_selectedBorder;
  protected Border m_activeBorder;

  protected Hashtable m_panes;
  protected ColorPane m_selected;

  public ColorMenu(String name) {
    super(name);
    m_unselectedBorder = new CompoundBorder(
      new MatteBorder(1, 1, 1, 1, getBackground()),
      new BevelBorder(BevelBorder.LOWERED, 
      Color.white, Color.gray));
    m_selectedBorder = new CompoundBorder(
      new MatteBorder(2, 2, 2, 2, Color.red),
      new MatteBorder(1, 1, 1, 1, getBackground()));
    m_activeBorder = new CompoundBorder(
      new MatteBorder(2, 2, 2, 2, Color.blue),
      new MatteBorder(1, 1, 1, 1, getBackground()));
        
    JPanel p = new JPanel();
    p.setBorder(new EmptyBorder(5, 5, 5, 5));
    p.setLayout(new GridLayout(8, 8));
    m_panes = new Hashtable();

    int[] values = new int[] { 0, 128, 192, 255 };
    for (int r=0; r<values.length; r++) {
      for (int g=0; g<values.length; g++) {
        for (int b=0; b<values.length; b++) {
          Color c = new Color(values[r], values[g], values[b]);
          ColorPane pn = new ColorPane(c);
          p.add(pn);
          m_panes.put(c, pn);
        }
      }
    }
    add(p);
  }

  public void setColor(Color c) {
    Object obj = m_panes.get(c);
    if (obj == null)
      return;
    if (m_selected != null)
      m_selected.setSelected(false);
    m_selected = (ColorPane)obj;
    m_selected.setSelected(true);
  }

  public Color getColor() {
    if (m_selected == null)
      return null;
    return m_selected.getColor();
  }

  public void doSelection() {
    fireActionPerformed(new ActionEvent(this, 
      ActionEvent.ACTION_PERFORMED, getActionCommand()));
  }

  class ColorPane extends JPanel implements MouseListener
  {
    protected Color m_c;
    protected boolean m_selected;

    public ColorPane(Color c) {
      m_c = c;
      setBackground(c);
      setBorder(m_unselectedBorder);
      String msg = "R "+c.getRed()+", G "+c.getGreen()+
        ", B "+c.getBlue();
      setToolTipText(msg);
      addMouseListener(this);
    }

    public Color getColor() { return m_c; }

    public Dimension getPreferredSize() {
      return new Dimension(15, 15);
    }
    public Dimension getMaximumSize() { return getPreferredSize(); }
    public Dimension getMinimumSize() { return getPreferredSize(); }

    public void setSelected(boolean selected) {
      m_selected = selected;
      if (m_selected)
        setBorder(m_selectedBorder);
      else
        setBorder(m_unselectedBorder);
    }

    public boolean isSelected() { return m_selected; }

    public void mousePressed(MouseEvent e) {}

    public void mouseClicked(MouseEvent e) {}

    public void mouseReleased(MouseEvent e) {
      setColor(m_c);
      MenuSelectionManager.defaultManager().clearSelectedPath();
      doSelection();
    }

    public void mouseEntered(MouseEvent e) {
      setBorder(m_activeBorder);
    }

    public void mouseExited(MouseEvent e) {
      setBorder(m_selected ? m_selectedBorder : 
        m_unselectedBorder);
    }
  }
}

Understanding the Code

Class BasicTextEditor

The createMenuBar() method now creates a new JMenu titled "Options" and populates it with two ColorMenus. The first of these menus receives an ActionListener which requests the selected color, using ColorMenu’s getColor() method, and assigns it as the foreground color of our editor component. Similarly, the second ColorMenu receives an ActionListener which manages our editor's background color.

Class ColorMenu

This class extends JMenu and represents a custom menu component which serves as a quick color chooser. Instance variables:

Border m_unselectedBorder: border to be used for a ColorPane (see below) when it is not selected and the mouse cursor is located outside of its bounds.

Border m_selectedBorder: border to be used for a ColorPane when it is selected and the mouse cursor is located outside of its bounds.

Border m_activeBorder: border to be used for a ColorPane when the mouse cursor is located inside its bounds.

Hashtable m_panes: a collection of ColorPanes.

ColorPane m_selected: a reference to the currently selected ColorPane.

The ColorMenu constructor takes a menu name as parameter and creates the underlying JMenu component using that name. This creates a root menu item which can be added to another menu or to a menu bar. Selecting this menu item will display its JPopupMenu component, which normally contains several simple menu items. In our case, however, we add a JPanel to it using JMenu’s add(Component c) method. This JPanel serves as a container for 64 ColorPanes (see below) which are used to display the available selectable colors, as well as the current selection. A triple for cycle is used to generate the constituent ColorPanes in 3-dimensional color space. Each ColorPane takes a Color instance as constructor parameter, and each ColorPane is placed in our Hashtable collection, m_panes, using its associated Color as the key.

The setColor() method finds a ColorPane which holds a given Color. If such a component is found this method clears the previously selected ColorPane and selects a new one by calling its setSelected() method. The getColor() method simply returns the currently selected color.

The doSelection() method sends an ActionEvent to registered listeners notifying them that an action has been performed on this ColorPane, which means a new color may have been selected.

Class ColorMenu.ColorPane

This inner class is used to display a single color available for selection in a ColorMenu. It extends JPanel and implements MouseListener to process its own mouse events. This class uses the three Border variables from the parent ColorMenu class to represent its state, whether selected, unselected, or active. Instance variables:

Color m_c: color instance represented by this pane.

boolean m_selected: a flag indicating whether or not this pane is currently selected.

The ColorPane constructor takes a Color instance as parameter and stores it in our m_c instance variable. The only thing we need to do to display that color is set it as the pane's background. We also add a tool tip indicating the red, green, and blue components of this color.

All MouseListener related methods should be familiar by now. However, take note of the mouseReleased() method which plays the key role in color selection: If the mouse is released over a ColorPane we first assign the associated Color to the parenting ColorMenu component using the setColor() method (so it later can be retrieved by any attached listeners). We then hide all opened menu components by calling the MenuSelectionManager.clearSelectedPath() method since menu selection is completed at this point. Finally we invoke the doSelection() method on the parenting ColorMenu component to notify all attached listeners.

Running the Code

Experiment with changing the editor’s background and foreground colors using our custom menu component available in the "Options" menu.. Note that a color selection will not affect anything until the mouse is released, and a mouse release also triggers the collapse of all menu popups in the current path. Figure 12.6 shows ColorMenu in action.

 

UI Guideline : Usability and Design alternatives

A more traditional approach to this exampe would be to have an elipsis on the menu option and open a Color Chooser Dialog. Consider what an improvement the presented design makes to usability. Within a limited range of colours, this design allows faster selection with the possible minor con that there is more chance of a mistake being made in the selection. However, such a mistake is low cost as it can easily be corrected. As you will see in the next chapter, knowing that you have a bounded range of input selections can be put to good use when improving a design and usability.

UI references:

Human Factors International at http://www.humanfactors.com

A Test to give you Fitt's at http://www.asktog.com