package com.explodingpixels.widgets.plaf; import java.awt.Dimension; import java.awt.Font; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionAdapter; import java.awt.event.MouseMotionListener; import javax.swing.ButtonGroup; import javax.swing.JCheckBoxMenuItem; import javax.swing.JComboBox; import javax.swing.JList; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.MenuElement; import javax.swing.MenuSelectionManager; import javax.swing.SwingUtilities; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import javax.swing.plaf.basic.ComboPopup; /** * An implementation of {@link javax.swing.plaf.basic.ComboPopup} that uses actual {@link javax.swing.JMenuItem}s rather than a * {@link javax.swing.JList} to display it's contents. */ public class EPComboPopup implements ComboPopup { private final JComboBox fComboBox; private JPopupMenu fPopupMenu = new JPopupMenu(); private Font fFont; private ComboBoxVerticalCenterProvider fComboBoxVerticalCenterProvider = new DefaultVerticalCenterProvider(); private static final int LEFT_SHIFT = 5; public EPComboPopup(JComboBox comboBox) { fComboBox = comboBox; fFont = comboBox.getFont(); fPopupMenu.addPopupMenuListener(createPopupMenuListener()); } /** * Creates a {@link javax.swing.event.PopupMenuListener} on the underlying {@link javax.swing.JPopupMenu} which forwards along the popup event * notification to the associated {@link javax.swing.JComboBox}. */ private PopupMenuListener createPopupMenuListener() { return new PopupMenuListener() { public void popupMenuWillBecomeVisible(PopupMenuEvent e) { fComboBox.firePopupMenuWillBecomeVisible(); } public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { fComboBox.firePopupMenuWillBecomeInvisible(); } public void popupMenuCanceled(PopupMenuEvent e) { fComboBox.firePopupMenuCanceled(); } }; } public void setFont(Font font) { fFont = font; } public void setVerticalComponentCenterProvider( ComboBoxVerticalCenterProvider comboBoxVerticalCenterProvider) { if (comboBoxVerticalCenterProvider == null) { throw new IllegalArgumentException("The given CompnonentCenterProvider cannot be null."); } fComboBoxVerticalCenterProvider = comboBoxVerticalCenterProvider; } private void togglePopup() { if (isVisible()) { hide(); } else { show(); } } private void clearAndFillMenu() { fPopupMenu.removeAll(); ButtonGroup buttonGroup = new ButtonGroup(); // add the given items to the popup menu. for (int i = 0; i < fComboBox.getModel().getSize(); i++) { Object item = fComboBox.getModel().getElementAt(i); JMenuItem menuItem = new JCheckBoxMenuItem(item.toString()); menuItem.setFont(fFont); menuItem.addActionListener(createMenuItemListener(item)); buttonGroup.add(menuItem); fPopupMenu.add(menuItem); // if the current item is selected, make the menu item reflect that. if (item.equals(fComboBox.getModel().getSelectedItem())) { menuItem.setSelected(true); fPopupMenu.setSelected(menuItem); } } fPopupMenu.pack(); int popupWidth = fComboBox.getWidth() + 5; // adjust the width to be slightly wider than the associated combo box. fPopupMenu.setSize(popupWidth, fPopupMenu.getHeight()); } private Point placePopupOnScreen() { // grab the right most location of the button. int buttonRightX = fComboBox.getWidth(); // figure out how the height of a menu item. Insets insets = fPopupMenu.getInsets(); // 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 - LEFT_SHIFT; int selectedItemIndex = fPopupMenu.getSelectionModel().getSelectedIndex(); int componentCenter = fComboBoxVerticalCenterProvider.provideCenter(fComboBox); int menuItemHeight = fPopupMenu.getComponent(selectedItemIndex).getPreferredSize().height; int menuItemCenter = insets.top + (selectedItemIndex * menuItemHeight) + menuItemHeight / 2; int y = componentCenter - menuItemCenter; // 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, fComboBox); if (bottomOfMenuOnScreen.y > size.height) { y = fComboBox.getHeight() - fPopupMenu.getPreferredSize().height; } return new Point(x, y); } private ActionListener createMenuItemListener(final Object comboBoxItem) { return new ActionListener() { public void actionPerformed(ActionEvent e) { fComboBox.setSelectedItem(comboBoxItem); } }; } private void forceCorrectPopupSelectionIfNeccessary() { if (fComboBox.getSelectedIndex() >= 0) { forceCorrectPopupSelection(); } } private void forceCorrectPopupSelection() { assert fPopupMenu.isShowing() : "The popup must be showing for this method to work properly."; // 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); } // ComboPopup implementation. ///////////////////////////////////////////////////////////////// public void show() { clearAndFillMenu(); // if there are combo box items, then show the popup menu. if (fComboBox.getModel().getSize() > 0) { // Point popupLocation = placePopupOnScreen(); Rectangle popupBounds = calculateInitialPopupBounds(); // fPopupMenu.show(fComboBox, popupLocation.x, popupLocation.y); fPopupMenu.show(fComboBox, popupBounds.x, popupBounds.y); forceCorrectPopupSelectionIfNeccessary(); } } public void hide() { fPopupMenu.setVisible(false); } public boolean isVisible() { return fPopupMenu.isVisible(); } /** * This method is not implemented and would throw an {@link UnsupportedOperationException} if * {@link javax.swing.plaf.basic.BasicComboBoxUI} didn't call it. Thus, this method should not * be used, as it always returns null. * * @return null. */ public JList getList() { return null; } public MouseListener getMouseListener() { return new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if (fComboBox.isEnabled()) { togglePopup(); } } @Override public void mouseReleased(MouseEvent e) { MenuSelectionManager.defaultManager().processMouseEvent(e); } }; } public MouseMotionListener getMouseMotionListener() { return new MouseMotionAdapter() { @Override public void mouseDragged(MouseEvent e) { MenuSelectionManager.defaultManager().processMouseEvent(e); } }; } public KeyListener getKeyListener() { return null; } public void uninstallingUI() { // TODO implement, if necessary. } // An interface to allow a third-party to provide the center of a given compoennt. //////////// public interface ComboBoxVerticalCenterProvider { int provideCenter(JComboBox comboBox); } // A default implementation of ComboBoxVerticalCenterProvider. //////////////////////////////// private static class DefaultVerticalCenterProvider implements ComboBoxVerticalCenterProvider { public int provideCenter(JComboBox comboBox) { return comboBox.getHeight() / 2; } } // Utility methods. /////////////////////////////////////////////////////////////////////////// private Rectangle calculateInitialPopupBounds() { // grab the right most location of the button. int comboBoxRightEdge = fComboBox.getWidth(); // figure out how the height of a menu item. Insets insets = fPopupMenu.getInsets(); // 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 = comboBoxRightEdge - fPopupMenu.getPreferredSize().width - LEFT_SHIFT; int selectedItemIndex = fPopupMenu.getSelectionModel().getSelectedIndex(); int componentCenter = fComboBoxVerticalCenterProvider.provideCenter(fComboBox); int menuItemHeight = fPopupMenu.getSelectionModel().getSelectedIndex() >= 0 ? fPopupMenu.getComponent(selectedItemIndex).getPreferredSize().height : 0; int menuItemCenter = insets.top + (selectedItemIndex * menuItemHeight) + menuItemHeight / 2; int y = componentCenter - menuItemCenter; // TODO this method doesn't robustly handle multiple monitors. Rectangle bounds = new Rectangle(new Point(x, y), fPopupMenu.getPreferredSize()); Dimension preferredSize = fPopupMenu.getPreferredSize(); // 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 + preferredSize.height); SwingUtilities.convertPointToScreen(bottomOfMenuOnScreen, fComboBox); if (bottomOfMenuOnScreen.y > size.height) { y = fComboBox.getHeight() - preferredSize.height; } Point position = new Point(x, y); return bounds; } }