package com.explodingpixels.widgets;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.MenuElement;
import javax.swing.MenuSelectionManager;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.Border;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
public class PopupButton<E> {
private static final ImageIcon ARROWS_ICON = new ImageIcon(
PopupButton.class.getResource(
"/com/explodingpixels/macwidgets/images/up_down_arrows_small.png"));
private JButton fButton = new CustomJButton();
private List<E> fPopupItemsList;
private E fSelectedItem;
private JPopupMenu fPopupMenu = new JPopupMenu();
public PopupButton(E selectedItem, List<E> popupItemsList) {
if (selectedItem == null) {
throw new IllegalArgumentException("The selected item cannot be " +
"null.");
}
if (popupItemsList == null) {
throw new IllegalArgumentException("The list of items to add to" +
"the popup menu cannot be null.");
}
if (!popupItemsList.contains(selectedItem)) {
throw new IllegalArgumentException("The item to select is not in" +
"the given list of items.");
}
fSelectedItem = selectedItem;
fPopupItemsList = popupItemsList;
init();
}
private void init() {
Font oldPopupMenuFont = fButton.getFont();
// TODO calculate font more robustly.
Font newPopupMenuFont = oldPopupMenuFont.deriveFont(
oldPopupMenuFont.getSize() - 2.0f);
ButtonGroup buttonGroup = new ButtonGroup();
// add the given items to the popup menu.
for (E item : fPopupItemsList) {
// TODO should we throw IllegalArgumentException if o is null?
JMenuItem menuItem = new JCheckBoxMenuItem(item.toString());
menuItem.setFont(newPopupMenuFont);
menuItem.addActionListener(createMenuItemListener(item));
buttonGroup.add(menuItem);
fPopupMenu.add(menuItem);
}
// set the selected item now that we've filled the popup menu with menu
// items.
setSelectedItem(fSelectedItem);
fPopupMenu.pack();
Font oldButtonFont = fButton.getFont();
Font newButtonFont = oldButtonFont.deriveFont(
oldButtonFont.getSize() - 2.0f);
fButton.setFont(newButtonFont);
fButton.setContentAreaFilled(false);
fButton.setHorizontalAlignment(SwingConstants.LEFT);
fButton.setBorder(BorderFactory.createEmptyBorder(2, 4, 2,
ARROWS_ICON.getIconWidth() + 10));
fButton.addActionListener(createButtonListener());
// figure out how big the button should be. we're using the menu to help
// us determine the width.
Border border = UIManager.getBorder("MenuItem.border");
Insets insets = border.getBorderInsets(new JMenuItem());
int width = fPopupMenu.getPreferredSize().width - insets.left - insets.right;
int height = fButton.getPreferredSize().height;
fButton.setPreferredSize(new Dimension(width, height));
}
public JComponent getComponent() {
return fButton;
}
private void setSelectedItem(E itemToSelect) {
fSelectedItem = itemToSelect;
((JMenuItem) fPopupMenu.getComponent(
fPopupItemsList.indexOf(fSelectedItem))).setSelected(true);
fButton.setText(fSelectedItem.toString());
}
private ActionListener createButtonListener() {
return new ActionListener() {
public void actionPerformed(ActionEvent e) {
// grab the right most location of the button.
int buttonRightX = fButton.getWidth();
// figure out how the height of a menu item.
Insets insets = fPopupMenu.getInsets();
int itemHeight_px = (fPopupMenu.getPreferredSize().height
- insets.top - insets.bottom) / fPopupItemsList.size();
// calculate the x and y value at which to place the popup menu.
// by default, this will place the selected menu item in the
// popup item directly over the button.
int x = buttonRightX - fPopupMenu.getPreferredSize().width;
int y = fButton.getY() - insets.top
- (fPopupItemsList.indexOf(fSelectedItem) * itemHeight_px);
// do a cursory check to make sure we're not placing the popup
// off the bottom of the screen. note that Java on Mac won't
// let the popup show up off screen no matter where you place it.
Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
Point bottomOfMenuOnScreen = new Point(0, y + fPopupMenu.getPreferredSize().height);
SwingUtilities.convertPointToScreen(bottomOfMenuOnScreen, fButton);
if (bottomOfMenuOnScreen.y > size.height) {
y = fButton.getHeight() - fPopupMenu.getPreferredSize().height;
}
// set the selected item in the popup menu.
fPopupMenu.setSelected(fPopupMenu.getComponent(
fPopupItemsList.indexOf(fSelectedItem)));
fPopupMenu.show(fButton, x, y);
// force the correct item to be shown as selected. this is a
// work around for Java bug 4740942, which has been fixed by
// Sun, but not by Apple.
int index = fPopupMenu.getSelectionModel().getSelectedIndex();
MenuElement[] menuPath = new MenuElement[2];
menuPath[0] = fPopupMenu;
menuPath[1] = fPopupMenu.getSubElements()[index];
MenuSelectionManager.defaultManager().setSelectedPath(menuPath);
}
};
}
private ActionListener createMenuItemListener(final E item) {
return new ActionListener() {
public void actionPerformed(ActionEvent e) {
setSelectedItem(item);
}
};
}
// Custom JButton. ////////////////////////////////////////////////////////////////////////////
private static class CustomJButton extends JButton {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
int x = getWidth() - ARROWS_ICON.getIconWidth() - 4;
int y = getHeight() / 2 - ARROWS_ICON.getIconHeight() / 2;
g.drawImage(ARROWS_ICON.getImage(), x, y, null);
}
}
}