// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.gui;
import java.awt.Component;
import java.awt.event.ItemEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
/**
* Provides a button component that pops up an associated menu when the button is pressed.
*/
public class ButtonPopupMenu extends JButton
{
public enum Align {
/** Show the menu below the button. */
TOP,
/** Show the menu on top of the button. */
BOTTOM,
}
private static final Comparator<JComponent> menuItemComparator = new Comparator<JComponent>() {
@Override
public int compare(JComponent item1, JComponent item2)
{
if (item1 instanceof JMenuItem && item2 instanceof JMenuItem) {
return ((JMenuItem)item1).getText().compareToIgnoreCase(((JMenuItem)item2).getText());
} else {
return 0;
}
}
};
private final JPopupMenu menu = new JPopupMenu();
private final PopupListener listener = new PopupListener();
private final PopupItemListener itemListener = new PopupItemListener();
private List<JComponent> items = new ArrayList<JComponent>();
private JMenuItem selected;
private Align menuAlign;
/**
* Constructs a new ButtonPopupMenu control without menu items. Use this constructor in conjunction
* with {@link #addItem(JMenuItem)} or {@link #addSeparator()}, if you want to have full control over the
* popup menu creation.
* @param text Text label for the button.
*/
public ButtonPopupMenu(String text)
{
this(text, (List<JMenuItem>)null, true, Align.TOP);
}
/**
* Constructs a new ButtonPopupMenu control without menu items. Use this constructor in conjunction
* with {@link #addItem(JMenuItem)} or {@link #addSeparator()}, if you want to have full control over the
* popup menu creation.
* @param text Text label for the button.
* @param align Indicates where to pop up the menu.
*/
public ButtonPopupMenu(String text, Align align)
{
this(text, (List<JMenuItem>)null, true, align);
}
/**
* Constructs a new ButtonPopupMenu control with the given menu items.
* @param text Text label for the button.
* @param menuItems List of menu items. Items will be sorted alphabetically before adding to the button.
*/
public ButtonPopupMenu(String text, JMenuItem[] menuItems)
{
this(text, menuItems, true, Align.TOP);
}
/**
* Constructs a new ButtonPopupMenu control with the given menu items.
* @param text Text label for the button.
* @param menuItems List of menu items.
* @param sorted Indicates whether to sort items alphabetically before adding to the button.
* @param align Indicates where to pop up the menu.
*/
public ButtonPopupMenu(String text, JMenuItem[] menuItems, boolean sorted, Align align)
{
super(text);
this.menuAlign = align;
setMenuItems(menuItems, sorted);
addMouseListener(listener);
}
/**
* Constructs a new ButtonPopupMenu control with the given menu items.
* @param text Text label for the button.
* @param menuItems List of menu items. Items will be sorted alphabetically before adding to the button.
*/
public ButtonPopupMenu(String text, List<JMenuItem> menuItems)
{
this(text, menuItems, true, Align.TOP);
}
/**
* Constructs a new ButtonPopupMenu control with the given menu items.
* @param text Text label for the button.
* @param menuItems List of menu items.
* @param sorted Indicates whether to sort items alphabetically before adding to the button.
* @param align Indicates where to pop up the menu.
*/
public ButtonPopupMenu(String text, List<JMenuItem> menuItems, boolean sorted, Align align)
{
super(text);
this.menuAlign = align;
setMenuItems(menuItems, sorted);
addMouseListener(listener);
}
public JMenuItem getSelectedItem()
{
return selected;
}
/**
* Replaces current list of menu items with the given list.
* @param menuItems List of menu items. Items will be sorted alphabetically before adding to the button.
*/
public void setMenuItems(JMenuItem[] menuItems)
{
setMenuItems(menuItems, true);
}
/**
* Replaces current list of menu items with the given list.
* @param menuItems List of menu items.
* @param sorted Indicates whether to sort items alphabetically before adding to the button.
*/
public void setMenuItems(JMenuItem[] menuItems, boolean sorted)
{
List<JMenuItem> list = new ArrayList<JMenuItem>();
if (menuItems != null) {
for (int i = 0; i < menuItems.length; i++) {
list.add(menuItems[i]);
}
}
setMenuItems(list, sorted);
}
/**
* Replaces current list of menu items with the given list.
* @param menuItems List of menu items. Items will be sorted alphabetically before adding to the button.
*/
public void setMenuItems(List<JMenuItem> menuItems)
{
setMenuItems(menuItems, true);
}
/**
* Replaces current list of menu items with the given list.
* @param menuItems List of menu items.
* @param sorted Indicates whether to sort items alphabetically before adding to the button.
*/
public void setMenuItems(List<JMenuItem> menuItems, boolean sorted)
{
removeAll();
if (menuItems != null) {
List<JMenuItem> preparedList;
if (sorted) {
preparedList = new ArrayList<JMenuItem>(menuItems);
Collections.sort(preparedList, menuItemComparator);
} else {
preparedList = menuItems;
}
for (final JMenuItem mi: preparedList) {
addItem(mi);
}
}
}
/**
* Returns an unfiltered read-only list of menu items of any class type.
*/
public List<? extends JComponent> getItems()
{
return Collections.unmodifiableList(items);
}
/**
* Returns a filtered list of JMenuItem instances. Instances of JSeparator or other class types
* will be filtered out.
*/
public List<JMenuItem> getMenuItems()
{
List<JMenuItem> list = new ArrayList<JMenuItem>(items.size());
for (final JComponent c: items) {
if (c instanceof JMenuItem) {
list.add((JMenuItem)c);
}
}
return list;
}
/** Returns the alignment of the menu relative to the button. */
public Align getMenuAlignment()
{
return menuAlign;
}
/** Sets how the menu should be aligned relative to the button. */
public void setMenuAlignment(Align align)
{
this.menuAlign = align;
}
/**
* Returns the JPopupMenu associated with this button.
*/
public JPopupMenu getPopupMenu()
{
return menu;
}
/**
* Appends the specified menu item to the end of the popup menu.
* @param item The JMenuItem to add.
* @return The JMenuItem added.
*/
public JMenuItem addItem(JMenuItem item)
{
if (item != null) {
items.add(item);
menu.add(item);
item.addMouseListener(itemListener);
return item;
}
return null;
}
/**
* Appends a new separator to the end of the popup menu.
*/
public void addSeparator()
{
JPopupMenu.Separator sep = new JPopupMenu.Separator();
items.add(sep);
menu.add(sep);
}
/**
* Removes the menu item at the specified index from the popup menu.
*/
public void remove(int pos)
{
if (pos < 0 || pos >= items.size()) {
throw new IllegalArgumentException();
}
JComponent c = items.remove(pos);
c.removeMouseListener(itemListener);
menu.remove(pos);
}
/**
* Removes all items from the popup menu.
*/
public void removeAll()
{
for (int i = items.size() - 1; i >= 0; i--) {
remove(i);
}
}
/**
* Returns the menu item at the specified index.
* @param pos Index of the menu item
* @return The menu item as JComponent.
*/
public JComponent getItemAt(int pos)
{
if (pos < 0 || pos >= items.size()) {
throw new IllegalArgumentException();
}
return items.get(pos);
}
/**
* Returns the position of the item in the popup menu. Returns -1 if not found.
* @param item The menu item.
* @return The position of the item in the popup menu, or -1 if not found.
*/
public int getItemIndex(JMenuItem item)
{
if (item != null) {
return items.indexOf(item);
}
return -1;
}
private void menuItemSelected(JMenuItem item)
{
if (item.isEnabled()) {
selected = item;
// Why won't the following line work?
fireItemStateChanged(new ItemEvent(this, ItemEvent.ITEM_STATE_CHANGED, selected, ItemEvent.SELECTED));
}
}
// -------------------------- INNER CLASSES --------------------------
private final class PopupItemListener extends MouseAdapter
{
private PopupItemListener()
{
}
@Override
public void mouseReleased(MouseEvent e)
{
if (!e.isPopupTrigger() && ((JMenuItem)e.getSource()).contains(e.getX(), e.getY()))
menuItemSelected((JMenuItem)e.getSource());
menu.setVisible(false);
}
@Override
public void mouseExited(MouseEvent e)
{
JMenuItem item = (JMenuItem)e.getSource();
item.setArmed(false);
item.repaint();
}
}
private final class PopupListener extends MouseAdapter
{
private PopupListener()
{
}
@Override
public void mousePressed(MouseEvent e)
{
if (!e.isPopupTrigger() && e.getComponent().isEnabled()) {
if (getMenuAlignment() == Align.BOTTOM) {
menu.show(e.getComponent(), 0, e.getComponent().getSize().height);
} else {
menu.show(e.getComponent(), 0, -menu.getPreferredSize().height);
}
}
}
@Override
public void mouseReleased(MouseEvent e)
{
if (!e.isPopupTrigger() && ((JButton)e.getSource()).contains(e.getX(), e.getY()) &&
e.getComponent().isEnabled()) {
if (getMenuAlignment() == Align.BOTTOM) {
menu.show(e.getComponent(), 0, e.getComponent().getSize().height);
} else {
menu.show(e.getComponent(), 0, -menu.getPreferredSize().height);
}
} else {
menu.setVisible(false);
Component components[] = menu.getComponents();
for (final Component component : components) {
if (component instanceof JMenuItem) {
JMenuItem item = (JMenuItem)component;
if (item.isArmed()) {
menuItemSelected(item);
break;
}
}
}
}
}
}
}