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).

 

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);

 

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.

 

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.

 

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</! FONT> 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).

 

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.

 

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.

 

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.

 

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).

 

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.

 

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).

 

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:

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:

Instance variables:

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).

 

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.

 

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).

 

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).

 

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).

 

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:

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:

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:

The SmallToggleButton constructor takes four arguments:

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).

 

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:

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:

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 references:

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

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