// Copyright (c) 2006 - 2008, Markus Strauch.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
package net.sf.sdedit.ui.components;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import javax.swing.Action;
import javax.swing.ImageIcon;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.KeyStroke;
/**
* This is a slightly advanced <tt>JMenuBar</tt> subclass that allows to
* structure a menu by means of category names, so a user is not required to
* build <tt>JMenu</tt>s. Furthermore, <tt>MenuBar</tt> provides a
* convenient way to define mnemonics for menu entries (by using the
* '&'-notation as known from Qt).
* <p>
* When an <tt>Action</tt> or <tt>JMenuItem</tt> is added to the
* <tt>MenuBar</tt>, a title string must be specified. If the string has a
* prefix that starts with '[' and ends with ']', the substring between these
* square brackets represents the accelerator key for the action or item. The
* appropriate key for the string is found as described here: <a
* href="http://java.sun.com/j2se/1.5.0/docs/api/javax/swing/KeyStroke.html#getKeyStroke(java.lang.String)">
* <tt>javax.swing.Keystroke#getKeyStroke(java.lang.String)</tt></a>.
* <p>
* If a title string contains an '&', the following character is used as the
* mnemonic for the entry or action. This also applies to categories/sub-menus.
*
* @author Markus Strauch
*/
public class MenuBar extends JMenuBar {
private static final long serialVersionUID = 0x1ED0BAA9;
private Map<String, JMenu> categories;
private Map<String, Action> nameMap;
/**
* Creates a new empty <tt>MenuBar</tt>.
*
*/
public MenuBar() {
super();
categories = new HashMap<String, JMenu>();
nameMap = new HashMap<String, Action>();
}
/**
* Returns an action that has formerly been added by
* {@linkplain #addAction(String, Action)} or
* {@linkplain #addAction(String, Action, int)}.
*
* @param name
* the name of the action
* @return the action with the name, or <tt>null</tt>
*/
public Action getActionByName(String name) {
return nameMap.get(name);
}
/**
* Creates an instance of a subclass of <tt>JMenuItem</tt> with the given
* title (possibly specifying an accelerator key or a mnemonic as described
* here: {@linkplain MenuBar}). The item is not added to any menu.
*
* @param <T>
* the type of the <tt>JMenuItem</tt> to be created
* @param title
* the title of the item
* @param itemType
* the type of the <tt>JMenuItem</tt>, represented as a
* <tt>Class</tt> object.
* @return an instance of a subclass of <tt>JMenuItem</tt>, bearing the
* given title
*/
public static <T extends JMenuItem> T makeMenuItem(String title,
Class<T> itemType) {
return makeMenuItem(title, title, String.class, itemType);
}
private static <T extends JMenuItem> T makeMenuItem(String title,
Object arg, Class<?> argType, Class<T> itemType) {
try {
KeyStroke acc = null;
if (title.charAt(0) == '[') {
int r = title.indexOf(']');
String stroke = title.substring(1, r);
acc = KeyStroke.getKeyStroke(stroke);
}
Constructor<T> constructor = itemType.getConstructor(argType);
int amp = title.indexOf('&');
T item = constructor.newInstance(new Object[] { arg });
item.setText(getCaption(title));
if (amp < title.length() - 1) {
item.setMnemonic(title.charAt(amp + 1));
}
if (acc != null) {
item.setAccelerator(acc);
}
return item;
} catch (Exception e) {
e.printStackTrace();
throw new InternalError();
}
}
private static String getCaption(String caption) {
// remove the prefix between square brackets
String _caption = caption;
if (caption.charAt(0) == '[') {
int r = caption.indexOf(']');
_caption = caption.substring(r + 1);
}
String c = "";
// delete the first occurrence of '&'
int amp = _caption.indexOf('&');
if (amp > 0) {
c = _caption.substring(0, amp);
}
if (amp < _caption.length() - 1) {
c += _caption.substring(amp + 1);
}
return c;
}
/**
* Removes an action from a category.
*
* @param category
* the name of the category, containing all '&'-characters
* @param action
* the action to be removed from the category
*/
public void removeAction(String category, Action action) {
JMenu categoryMenu = categories.get(getCaption(category));
if (categoryMenu == null) {
return;
}
for (int i = 0; i < categoryMenu.getItemCount(); i++) {
JMenuItem item = categoryMenu.getItem(i);
if (item.getAction() == action) {
categoryMenu.remove(i);
return;
}
}
}
/**
* Adds an <tt>Action</tt> to a (sub-)menu of this <tt>MenuBar</tt>.
* The <tt>Action</tt> must have a name (as set via
* <tt>Action.putValue(Action.NAME,name)</tt>), which may specify a
* mnemonic and accelerator key as described in the class comment:
* {@linkplain #MenuBar}.
* <p>
* Example: Let <tt>a</tt> be an action to be added to the submenu
* "foo" of "bar". This can be done so:
* <tt>addAction("foo.bar",a,0)</tt>. If "foo" and
* "bar" should have "f" resp. "b" as
* mnemonics, you would call
* <tt>addAction("&foo.&bar",a,0)</tt>
*
* @param category
* the string describing the category, where names belonging to
* adjacent category levels are separated by '.'. Category names
* may include '&' for mnemonics.
* @param action
* the action to be added
* @param pos
* the position of the action in the (sub-)menu
* @return the <tt>JMenuItem</tt> instance associated to the action
* @throws IllegalArgumentException
* if an illegal (sub-)category is specified
*/
public JMenuItem addAction(String category, Action action, int pos) {
JMenu categoryMenu = categories.get(getCaption(category));
if (categoryMenu == null) {
int dot = category.lastIndexOf('.');
if (dot == -1) {
categoryMenu = makeMenuItem(category, JMenu.class);
add(categoryMenu);
categories.put(getCaption(category), categoryMenu);
} else {
String subCategory = category.substring(dot + 1);
String supCategory = category.substring(0, dot);
JMenu supMenu = categories.get(getCaption(supCategory));
if (supMenu == null) {
throw new IllegalArgumentException("menu for category "
+ supCategory + " must"
+ " exist before a subcategory " + subCategory
+ " can be added");
}
categoryMenu = makeMenuItem(subCategory, JMenu.class);
supMenu.add(categoryMenu);
categories.put(getCaption(category), categoryMenu);
}
}
JMenuItem item = null;
if (action != null) {
nameMap.put((String) action.getValue(Action.NAME), action);
item = makeMenuItem((String) action.getValue(Action.NAME), action,
Action.class, JMenuItem.class);
if (pos == -1) {
categoryMenu.add(item);
} else {
categoryMenu.insert(item, pos);
}
}
return item;
}
/**
* Adds a new (sub-)menu to this <tt>MenuBar</tt>. Menus can be
* arbitrarily nested. When a menu for a category on the n n-th (n >= 1)
* level is added, the category string must contain n substrings, separated
* by '.', describing all higher categories in descending order and finally
* the n-th category. All submenus for the categories on the levels 1, ...,
* n-1 must already be present.
*
* @param category
* the category of the (sub-)menu (represented by a string with
* '.''s as described above)
* @param icon
* an icon for the (sub-)menu or <tt>null</tt>
*/
public void addMenu(String category, ImageIcon icon) {
int dot = category.lastIndexOf('.');
JMenu categoryMenu;
if (dot == -1) {
categoryMenu = makeMenuItem(category, JMenu.class);
if (icon != null) {
categoryMenu.setIcon(icon);
}
add(categoryMenu);
categories.put(getCaption(category), categoryMenu);
} else {
String subCategory = category.substring(dot + 1);
String supCategory = category.substring(0, dot);
JMenu supMenu = categories.get(getCaption(supCategory));
if (supMenu == null) {
throw new IllegalArgumentException("menu for category "
+ supCategory + " must"
+ " exist before a subcategory " + subCategory
+ " can be added");
}
categoryMenu = makeMenuItem(subCategory, JMenu.class);
supMenu.add(categoryMenu);
categories.put(getCaption(category), categoryMenu);
}
if (icon != null) {
categoryMenu.setIcon(icon);
}
}
/**
* Adds an action in the given category. See also
* {@linkplain #addAction(String, Action, int)}.
*
* @param category
* the category of the action
* @param action
* the action to be added
* @return the <tt>JMenuItem</tt> associated to the action
* @throws IllegalArgumentException
* if the category is part of a non-existing higher level
* category
*/
public JMenuItem addAction(String category, Action action) {
return addAction(category, action, -1);
}
/**
* Adds an item to the given category. See also
* {@linkplain #addAction(String, Action, int)}.
*
* @param category
* the category of the action
* @param item
* the item to be added
* @throws IllegalArgumentException
* if the category is part of a non-existing higher level
* category
*/
public void addItem(String category, JMenuItem item) {
// TODO insert category menu if needed
JMenu categoryMenu = categories.get(getCaption(category));
categoryMenu.add(item);
}
/**
* @see javax.swing.JMenuBar#updateUI()
*/
@Override
public void updateUI() {
super.updateUI();
if (categories != null) {
for (JMenu menu : categories.values()) {
menu.updateUI();
for (int i = 0; i < menu.getItemCount(); i++) {
menu.getItem(i).updateUI();
}
}
}
}
}