Introduction
Every Graphical User Interface (GUI) development includes a good chunk of navigational code, which could be very messy depending on the size and functionality of the interface.
UI navigation includes top-level menus, sub-menus, popup menus and toolbars. The menu names, tool tips often require internationalization. Adding/removing menus, menu items from the resource bundle with fewer objects eliminate lot of code development time and the maintenance.
How to solve it
The solution proposed here contains two parts.
1. Resource file structure: which shows how to organize the entire menus, submenus and menu items and their properties.
2. Code to build the Menu: which shows how to read the resources from the resource bundle in a recursive way with out writing too much code. This code is not only out of the way of your UI but also helps to achieve Internationalization
Resource File Structure
The resource structure for modeling the menus is shown below:
#SEP is a keyword to have a separator between menus or menu items
MyApp.MenuBar.Menu=File,SEP,Help
MyApp.MenuBar.File.Mnemonic=VK_F
MyApp.MenuBar.File.Label=File
#
MyApp.MenuBar.File.Menu=New,SEP,Open,SEP,Save,SaveAll,SEP,Exit
MyApp.MenuBar.File.Menu.New.Mnemonic=VK_N
MyApp.MenuBar.File.Menu.New.Label=New...
MyApp.MenuBar.File.Menu.Open.Mnemonic=VK_O
MyApp.MenuBar.File.Menu.Open.Label=Open Module...
MyApp.MenuBar.File.Menu.Save.Mnemonic=VK_S
MyApp.MenuBar.File.Menu.Save.Label=Save Module...
MyApp.MenuBar.File.Menu.SaveAll.Mnemonic=VK_A
MyApp.MenuBar.File.Menu.SaveAll.Label=Save All Modules...
MyApp.MenuBar.File.Menu.Exit.Mnemonic=VK_E
MyApp.MenuBar.File.Menu.Exit.Label=Exit
#
MyApp.MenuBar.Help.Mnemonic=VK_H
MyApp.MenuBar.Help.Label=Help
MyApp.MenuBar.Help.Menu=Index,SEP,About
MyApp.MenuBar.Help.Menu.Index.Mnemonic=VK_I
MyApp.MenuBar.Help.Menu.Index.Label=Index
MyApp.MenuBar.Help.Menu.About.Mnemonic=VK_A
MyApp.MenuBar.Help.Menu.About.Label=About...
Download this source file...
Code to build the menu
The object is derived from a javax.swing.JMenuBar. The constructor of the object basically takes the main resource for the menu instance. In the above example it is shown as (MyApp.MenuBar). The constructor passes a “null” parent menu and the main resource and a submenu option string, which is derived from the main resource string by appending a constant string “.Menu” (in the above example: MyApp.MenuBar.Menu) to a recursive routine called populateMenus.
The routine populateMenus parses the resource value of submenu option string (in this example: MyApp.MenuBar.Menu) using a StringTokenizer with the help of “,|:” delimiter string. Every token is appended to the submenu option string and a constant string of “.Menu” to see if this new token contains any submenus (for example: MyApp.MenuBar.Menu.File.Menu – where File is the token here). If the resource value comes out to be null then it will be used as a simple MenuItem and it is added to the given parent menu instance. If the parent menu is a null object then that MenuItem will be added to the “this” instance, which is a subclass of JMenuBar. If the resource value for the new token submenu comes out to be non-null then it will be passed to the populateMenus to process that submenu recursively.
There are some convenience routines added to this object to find a given menu item in a given menu, enable a menu or a menu item etc.
Complete source for this object to build a MenuBar object is attached as appendix.
Add XML Flavor
The same resource(s) for building a menu bar can be described in XML. The details for parsing the XML and building a recursive routine is intentionally left out in this article.
Example XML file for describing a MenuBar is given below:
<MenuItems>
<Menu Name="File" Label="File">
<Mnemonic>VK_F</Mnemonic>
<Enabled>true</Enabled>
<MenuItem Name="Open" Label="Open...">
<ActionCommand>FileOpen</ActionCommand>
<ActionClass>com.mycompany.actions.FileOpen</ActionClass>
<Enabled>true</Enabled>
<Mnemonic>VK_O</Mnemonic>
</UserData>
</MenuItemType>
</MenuItem>
<Menu Name="Save" Label="Save">
<Mnemonic>VK_S</Mnemonic>
<Enabled>true</Enabled>
<MenuItem Name="SaveAs" Label="Save As...">
<ActionCommand>FileSaveAs</ActionCommand>
<ActionClass>com.mycompany.actions.FileSaveAs</ActionClass>
<Enabled>true</Enabled>
<Mnemonic>VK_S</Mnemonic>
</UserData>
</MenuItemType>
</MenuItem>
<MenuItem Name="SaveAs" Label="Save All...">
<ActionCommand>FileSaveAll</ActionCommand>
<ActionClass>com.mycompany.actions.FileSaveAll</ActionClass>
<Enabled>true</Enabled>
<Mnemonic>VK_A</Mnemonic>
</UserData>
</MenuItemType>
</MenuItem>
</Menu>
</Menu>
</MenuItems>
Download this source file...
Appendix
Source code for LMenuBar. [Localized MenuBar]:
import java.awt.*;
import javax.swing.*;
import java.util.*;
public class LMenuBar extends JMenuBar
{
/** Menu Vector */
private Vector jMenus;
/** Menu Item Vector */
private Vector jMenuItems;
/** Resource bundle */
ResourceBundle bundle;
/**
* Default Constructor - this is the main menu bar for the application
* [defaults to resource: LMenuBar.MenuOptions]
*/
public LMenuBar()
{
this("LMenuBar.MenuOptions");
}
/**
* This is the sub menu bar for the application - if auxiliary frames
* need menus then we need a way to do it!
* @param resourceName Menu Bar Option Resource Name
* [ex: myframe.LMenuBar.MenuOptions]
*/
public LMenuBar(String resourceName)
{
/** Resource Bundle object */
bundle = ResourceBundle.getBundle(“MyAppBundle”);
this.setBorderPainted(true);
jMenus = new Vector();
jMenuItems = new Vector();
String menuOpts=bundle.getString(resourceName+".Menu");
// recursive routine to exhaust all the menu items...
populateMenus(null, menuOpts, resourceName);
}
/**
* Recursive routine - just walks thru the comma,pipe,colon separated
* items from the Resource Bundle and builds recursively all the
* menus, submenus, and menuitems underneath.
*
* @param parent parent menu
* @param menuOpts delimited menu options
* @param resName resource name, hierarchically appended
*/
private void populateMenus(JMenu parent,String menuOpts,String resName)
{
// if the options string is nothing - just return!
if( menuOpts.trim().length() < 2 )
return;
// tokenize the options string and walk thru it
StringTokenizer st = new StringTokenizer(menuOpts,"|,:");
int count = st.countTokens();
for(int i=0; i
String item = (String)st.nextElement();
if( item.equalsIgnoreCase("SEP") )
{
if( parent == null )
this.add(Box.createHorizontalGlue());
else
{
parent.add( new JSeparator() );
}
continue;
}
String subMenus = bundle.getString(resName+"."+item+".Menu");
// if the submenu string is empty - then it's just a
// menu item otherwise it is a submenu...
if( subMenus.trim().length() < 2 )
{
String cutItem = cutInvalidChars(item);
boolean cb = bundle.getBoolean(
resName+"."+cutItem+".CheckBox");
// do I have a Label for this???
String lbl = bundle.getString(
resName+"."+cutItem+".Label");
if( lbl == null || lbl.trim().length() < 1 )
lbl = item;
JMenuItem menuItem;
if( cb )
{
menuItem = new JCheckBoxMenuItem(lbl);
}
else
menuItem = new JMenuItem(lbl);
menuItem.setEnabled(false);
int mnemonic = bundle.getMnemonic(
resName+"."+cutItem+".Mnemonic");
menuItem.setMnemonic(mnemonic);
if ( parent == null )
{
menuItem.setActionCommand(cutItem);
this.add(menuItem);
}
else
{
// Since the menu Items for the Subsciber,
// Site and Service menus are the same ...
// Qualify them with the parent Menu..
String pCmd = parent.getActionCommand();
menuItem.setActionCommand(cutItem);
parent.add(menuItem);
}
jMenuItems.add(menuItem);
}
else
{
// do I have a Label for this???
String cutItem = cutInvalidChars(item);
String lbl = bundle.getString(
resName+"."+cutItem+".Label");
if( lbl == null || lbl.trim().length() < 1 )
lbl = item;
JMenu submenu = new JMenu(lbl);
submenu.setActionCommand(cutItem);
submenu.setEnabled(false);
if( parent == null )
this.add(submenu);
else
parent.add(submenu);
int mnemonic = bundle.getMnemonic(
resName+"."+cutItem+".Mnemonic");
submenu.setMnemonic(mnemonic);
jMenus.add(submenu);
// exhaust all the children of this submenu
populateMenus(submenu, subMenus,
resName+"."+cutItem+".Menu");
}
}
}
/**
* Menu/Menu Item Names may contain spaces and special parsable chars
* like '.' which can't be inserted in ResourceBundles hence we need
* to tailor the item name to match exactly the one put in the bundle.
* @param item resource
*/
public static String cutInvalidChars(String item)
{
String itm = item;
itm = itm.replace('.', ' ');
itm = itm.trim();
StringTokenizer st = new StringTokenizer(itm, " ");
String tmp=" ";
while(st.hasMoreTokens())
tmp += (String)st.nextToken();
tmp = tmp.trim();
if( tmp.length() > 1 )
return tmp;
else
return itm;
}
/**
* Let someone else take care of the actions on the submenu items
* @param al object who takes care of the actions!
*/
public void setActionListener(ActionListener al)
{
for(int i=0; i
}
/**
* Add a specific ActionListener to specific menu option
* @param m Menu name
* @param mitem MenuItem name
* @param al object who takes care of the actions!
*/
public void setActionListener(String m, String mitem, ActionListener al)
{
JMenuItem item = getMenuItem(m, mitem);
if( item == null )
return;
item.addActionListener(al);
}
/**
* Get the JMenu for a given menu name
* @param menuName Menu Name (Unique in the entire menu!)
*/
public JMenu getMenu(String menuName)
{
for(int i=0; i
JMenu menu = (JMenu)jMenus.elementAt(i);
if( menu.getText().equalsIgnoreCase(menuName) ||
menu.getActionCommand().equalsIgnoreCase(menuName) )
{
return menu;
}
}
return null;
}
/**
* Get the JMenu for a given menu name
* @param menuName Menu Name (Unique in the entire menu!)
* @param menuItemName Menu Item Name (Unique in the given menu!)
*/
public JMenuItem getMenuItem(String menuName, String menuItemName)
{
// find the menu for the given menuName
JMenu selMenu=getMenu(menuName);
if( selMenu != null )
{
// find the menu item for the given menuItemName
// under this menu
for(int i=0; i
Object menuItem = selMenu.getMenuComponent(i);
if( menuItem instanceof JMenuItem &&
(((JMenuItem)menuItem).getText().startsWith(
menuItemName) || ((JMenuItem)menuItem
).getActionCommand().equalsIgnoreCase(
menuItemName)) )
{
return (JMenuItem)menuItem;
}
}
}
return null;
}
/** Convenience routines to make a menu enabled or disabled
* @param menuName menu name (unique in the entire menu!)
* @param flag true enables it, false disables it
*/
public void setEnableMenu(String menuName, boolean flag)
{
JMenu menu = getMenu(menuName);
if( menu != null )
menu.setEnabled( flag );
}
/** Convenience routines to make a menuitem enabled or disabled
* @param menuName menu name
* @param menuItemName menuitem name
* @param flag true enables it, false disables it
*/
public void setEnableMenuItem(
String menuName, String menuItemName, boolean flag)
{
// find the menu for the given menuName
JMenuItem selMenuItem=getMenuItem(menuName, menuItemName);
if( selMenuItem != null )
selMenuItem.setEnabled( flag );
}
/**
* Adds a custom Action handler for a menu or menuitem
* @param actionTbl Action Handler Table
*/
public void setActions(Hashtable actionTbl)
{
if( actionTbl == null || actionTbl.size() < 1 )
return;
for(int i=0; i
JMenuItem item = (JMenuItem)jMenuItems.elementAt(i);
String label = item.getActionCommand();
Action action = (Action)actionTbl.get(label.toUpperCase());
if (action != null )
item.setAction(action);
}
}
/** Disable all menus/menuitems */
public void disableAllMenuItems()
{
for (int k=0; k
if (getMenu(k) != null)
getMenu(k).setEnabled(false);
}
}
/** Enable all menus/menuitems */
public void enableAllMenuItems()
{
for (int k=0; k
((JMenu)jMenus.get(k)).setEnabled(true);
}
for (int k=0; k
((JMenuItem)jMenuItems.get(k)).setEnabled(true);
}
}
}
Download this source file...