/** * MenuScroller.java 1.5.0 04/02/12 * License: use / modify without restrictions (see https://tips4java.wordpress.com/about/) * Heavily modified for JOSM needs => drop unused features and replace static scrollcount approach by dynamic behaviour */ package org.openstreetmap.josm.gui; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.util.Arrays; import javax.swing.Icon; import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JSeparator; import javax.swing.Timer; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.tools.WindowGeometry; /** * A class that provides scrolling capabilities to a long menu dropdown or * popup menu. A number of items can optionally be frozen at the top of the menu. *

* Implementation note: The default scrolling interval is 150 milliseconds. *

* @author Darryl, https://tips4java.wordpress.com/2009/02/01/menu-scroller/ * @since 4593 */ public class MenuScroller { private JPopupMenu menu; private Component[] menuItems; private MenuScrollItem upItem; private MenuScrollItem downItem; private final MenuScrollListener menuListener = new MenuScrollListener(); private final MouseWheelListener mouseWheelListener = new MouseScrollListener(); private int interval; private int topFixedCount; private int firstIndex; private static final int ARROW_ICON_HEIGHT = 10; private int computeScrollCount(int startIndex) { int result = 15; if (menu != null) { // Compute max height of current screen int maxHeight = WindowGeometry.getMaxDimensionOnScreen(menu).height - ((JFrame) Main.parent).getInsets().top; // Remove top fixed part height if (topFixedCount > 0) { for (int i = 0; i < topFixedCount; i++) { maxHeight -= menuItems[i].getPreferredSize().height; } maxHeight -= new JSeparator().getPreferredSize().height; } // Remove height of our two arrow items + insets maxHeight -= menu.getInsets().top; maxHeight -= upItem.getPreferredSize().height; maxHeight -= downItem.getPreferredSize().height; maxHeight -= menu.getInsets().bottom; // Compute scroll count result = 0; int height = 0; for (int i = startIndex; i < menuItems.length && height <= maxHeight; i++, result++) { height += menuItems[i].getPreferredSize().height; } if (height > maxHeight) { // Remove extra item from count result--; } else { // Increase scroll count to take into account upper items that will be displayed // after firstIndex is updated for (int i = startIndex-1; i >= 0 && height <= maxHeight; i--, result++) { height += menuItems[i].getPreferredSize().height; } if (height > maxHeight) { result--; } } } return result; } /** * Registers a menu to be scrolled with the default scrolling interval. * * @param menu the menu * @return the MenuScroller */ public static MenuScroller setScrollerFor(JMenu menu) { return new MenuScroller(menu); } /** * Registers a popup menu to be scrolled with the default scrolling interval. * * @param menu the popup menu * @return the MenuScroller */ public static MenuScroller setScrollerFor(JPopupMenu menu) { return new MenuScroller(menu); } /** * Registers a menu to be scrolled, with the specified scrolling interval. * * @param menu the menu * @param interval the scroll interval, in milliseconds * @return the MenuScroller * @throws IllegalArgumentException if scrollCount or interval is 0 or negative * @since 7463 */ public static MenuScroller setScrollerFor(JMenu menu, int interval) { return new MenuScroller(menu, interval); } /** * Registers a popup menu to be scrolled, with the specified scrolling interval. * * @param menu the popup menu * @param interval the scroll interval, in milliseconds * @return the MenuScroller * @throws IllegalArgumentException if scrollCount or interval is 0 or negative * @since 7463 */ public static MenuScroller setScrollerFor(JPopupMenu menu, int interval) { return new MenuScroller(menu, interval); } /** * Registers a menu to be scrolled, with the specified scrolling interval, * and the specified numbers of items fixed at the top of the menu. * * @param menu the menu * @param interval the scroll interval, in milliseconds * @param topFixedCount the number of items to fix at the top. May be 0. * @return the MenuScroller * @throws IllegalArgumentException if scrollCount or interval is 0 or * negative or if topFixedCount is negative * @since 7463 */ public static MenuScroller setScrollerFor(JMenu menu, int interval, int topFixedCount) { return new MenuScroller(menu, interval, topFixedCount); } /** * Registers a popup menu to be scrolled, with the specified scrolling interval, * and the specified numbers of items fixed at the top of the popup menu. * * @param menu the popup menu * @param interval the scroll interval, in milliseconds * @param topFixedCount the number of items to fix at the top. May be 0 * @return the MenuScroller * @throws IllegalArgumentException if scrollCount or interval is 0 or * negative or if topFixedCount is negative * @since 7463 */ public static MenuScroller setScrollerFor(JPopupMenu menu, int interval, int topFixedCount) { return new MenuScroller(menu, interval, topFixedCount); } /** * Constructs a MenuScroller that scrolls a menu with the * default scrolling interval. * * @param menu the menu * @throws IllegalArgumentException if scrollCount is 0 or negative */ public MenuScroller(JMenu menu) { this(menu, 150); } /** * Constructs a MenuScroller that scrolls a popup menu with the * default scrolling interval. * * @param menu the popup menu * @throws IllegalArgumentException if scrollCount is 0 or negative */ public MenuScroller(JPopupMenu menu) { this(menu, 150); } /** * Constructs a MenuScroller that scrolls a menu with the * specified scrolling interval. * * @param menu the menu * @param interval the scroll interval, in milliseconds * @throws IllegalArgumentException if scrollCount or interval is 0 or negative * @since 7463 */ public MenuScroller(JMenu menu, int interval) { this(menu, interval, 0); } /** * Constructs a MenuScroller that scrolls a popup menu with the * specified scrolling interval. * * @param menu the popup menu * @param interval the scroll interval, in milliseconds * @throws IllegalArgumentException if scrollCount or interval is 0 or negative * @since 7463 */ public MenuScroller(JPopupMenu menu, int interval) { this(menu, interval, 0); } /** * Constructs a MenuScroller that scrolls a menu with the * specified scrolling interval, and the specified numbers of items fixed at * the top of the menu. * * @param menu the menu * @param interval the scroll interval, in milliseconds * @param topFixedCount the number of items to fix at the top. May be 0 * @throws IllegalArgumentException if scrollCount or interval is 0 or * negative or if topFixedCount is negative * @since 7463 */ public MenuScroller(JMenu menu, int interval, int topFixedCount) { this(menu.getPopupMenu(), interval, topFixedCount); } /** * Constructs a MenuScroller that scrolls a popup menu with the * specified scrolling interval, and the specified numbers of items fixed at * the top of the popup menu. * * @param menu the popup menu * @param interval the scroll interval, in milliseconds * @param topFixedCount the number of items to fix at the top. May be 0 * @throws IllegalArgumentException if scrollCount or interval is 0 or * negative or if topFixedCount is negative * @since 7463 */ public MenuScroller(JPopupMenu menu, int interval, int topFixedCount) { if (interval <= 0) { throw new IllegalArgumentException("interval must be greater than 0"); } if (topFixedCount < 0) { throw new IllegalArgumentException("topFixedCount cannot be negative"); } upItem = new MenuScrollItem(MenuIcon.UP, -1); downItem = new MenuScrollItem(MenuIcon.DOWN, +1); setInterval(interval); setTopFixedCount(topFixedCount); this.menu = menu; menu.addPopupMenuListener(menuListener); menu.addMouseWheelListener(mouseWheelListener); } /** * Returns the scroll interval in milliseconds * * @return the scroll interval in milliseconds */ public int getInterval() { return interval; } /** * Sets the scroll interval in milliseconds * * @param interval the scroll interval in milliseconds * @throws IllegalArgumentException if interval is 0 or negative */ public void setInterval(int interval) { if (interval <= 0) { throw new IllegalArgumentException("interval must be greater than 0"); } upItem.setInterval(interval); downItem.setInterval(interval); this.interval = interval; } /** * Returns the number of items fixed at the top of the menu or popup menu. * * @return the number of items */ public int getTopFixedCount() { return topFixedCount; } /** * Sets the number of items to fix at the top of the menu or popup menu. * * @param topFixedCount the number of items */ public void setTopFixedCount(int topFixedCount) { if (firstIndex <= topFixedCount) { firstIndex = topFixedCount; } else { firstIndex += (topFixedCount - this.topFixedCount); } this.topFixedCount = topFixedCount; } /** * Removes this MenuScroller from the associated menu and restores the * default behavior of the menu. */ public void dispose() { if (menu != null) { menu.removePopupMenuListener(menuListener); menu.removeMouseWheelListener(mouseWheelListener); menu.setPreferredSize(null); menu = null; } } /** * Ensures that the dispose method of this MenuScroller is * called when there are no more refrences to it. * * @throws Throwable if an error occurs. * @see MenuScroller#dispose() */ @Override protected void finalize() throws Throwable { dispose(); super.finalize(); } private void refreshMenu() { if (menuItems != null && menuItems.length > 0) { int allItemsHeight = 0; for (Component item : menuItems) { allItemsHeight += item.getPreferredSize().height; } int allowedHeight = WindowGeometry.getMaxDimensionOnScreen(menu).height - ((JFrame) Main.parent).getInsets().top; boolean mustSCroll = allItemsHeight > allowedHeight; if (mustSCroll) { firstIndex = Math.max(topFixedCount, firstIndex); int scrollCount = computeScrollCount(firstIndex); firstIndex = Math.min(menuItems.length - scrollCount, firstIndex); upItem.setEnabled(firstIndex > topFixedCount); downItem.setEnabled(firstIndex + scrollCount < menuItems.length); menu.removeAll(); for (int i = 0; i < topFixedCount; i++) { menu.add(menuItems[i]); } if (topFixedCount > 0) { menu.addSeparator(); } menu.add(upItem); for (int i = firstIndex; i < scrollCount + firstIndex; i++) { menu.add(menuItems[i]); } menu.add(downItem); int preferredWidth = 0; for (Component item : menuItems) { preferredWidth = Math.max(preferredWidth, item.getPreferredSize().width); } menu.setPreferredSize(new Dimension(preferredWidth, menu.getPreferredSize().height)); } else if (!Arrays.equals(menu.getComponents(), menuItems)) { // Scroll is not needed but menu is not up to date menu.removeAll(); for (Component item : menuItems) { menu.add(item); } } menu.revalidate(); menu.repaint(); } } private class MenuScrollListener implements PopupMenuListener { @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { setMenuItems(); } @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { restoreMenuItems(); } @Override public void popupMenuCanceled(PopupMenuEvent e) { restoreMenuItems(); } private void setMenuItems() { menuItems = menu.getComponents(); refreshMenu(); } private void restoreMenuItems() { menu.removeAll(); for (Component component : menuItems) { menu.add(component); } } } private class MenuScrollTimer extends Timer { MenuScrollTimer(final int increment, int interval) { super(interval, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { firstIndex += increment; refreshMenu(); } }); } } private class MenuScrollItem extends JMenuItem implements ChangeListener { private final MenuScrollTimer timer; MenuScrollItem(MenuIcon icon, int increment) { setIcon(icon); setDisabledIcon(icon); timer = new MenuScrollTimer(increment, interval); addChangeListener(this); } public void setInterval(int interval) { timer.setDelay(interval); } @Override public void stateChanged(ChangeEvent e) { if (isArmed() && !timer.isRunning()) { timer.start(); } if (!isArmed() && timer.isRunning()) { timer.stop(); } } } private enum MenuIcon implements Icon { UP(9, 1, 9), DOWN(1, 9, 1); private static final int[] XPOINTS = {1, 5, 9}; private final int[] yPoints; MenuIcon(int... yPoints) { this.yPoints = yPoints; } @Override public void paintIcon(Component c, Graphics g, int x, int y) { Dimension size = c.getSize(); Graphics g2 = g.create(size.width / 2 - 5, size.height / 2 - 5, 10, 10); g2.setColor(Color.GRAY); g2.drawPolygon(XPOINTS, yPoints, 3); if (c.isEnabled()) { g2.setColor(Color.BLACK); g2.fillPolygon(XPOINTS, yPoints, 3); } g2.dispose(); } @Override public int getIconWidth() { return 0; } @Override public int getIconHeight() { return ARROW_ICON_HEIGHT; } } private class MouseScrollListener implements MouseWheelListener { @Override public void mouseWheelMoved(MouseWheelEvent mwe) { firstIndex += mwe.getWheelRotation(); refreshMenu(); if (Main.isDebugEnabled()) { Main.debug(getClass().getName()+" consuming event "+mwe); } mwe.consume(); } } }