/** * @(#)MenuScroller.java 1.3.0 05/04/09 */ package com.dmdirc.addons.ui_swing.components; 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.MouseEvent; import java.awt.event.MouseListener; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JSeparator; import javax.swing.MenuSelectionManager; import javax.swing.Timer; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.MenuEvent; import javax.swing.event.MenuListener; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; /** * 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 and/or * bottom of the menu. *
* Implementation note: The default number of items to display * at a time is 15, and the default scrolling interval is 125 milliseconds. *
* @author Darryl */ public class MenuScroller { private JMenu menu; private JPopupMenu popupMenu; private Component[] menuItems; private MenuScrollItem upItem; private MenuScrollItem downItem; private final MenuScrollListener menuListener = new MenuScrollListener(); private int scrollCount; private int interval; private int topFixedCount; private int bottomFixedCount; private int firstIndex = 0; private boolean showSeperators = true; /** * Registers a menu to be scrolled with the default number of items to * display at a time and 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 number of items to * display at a time and 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 default number of items to * display at a time and the specified scrolling interval. *
* @param menu the menu * @param scrollCount the number of items to display at a time * @return the MenuScroller * @throws IllegalArgumentException if scrollCount is 0 or negative */ public static MenuScroller setScrollerFor(JMenu menu, int scrollCount) { return new MenuScroller(menu, scrollCount); } /** * Registers a popup menu to be scrolled with the default number of items to * display at a time and the specified scrolling interval. *
* @param menu the popup menu * @param scrollCount the number of items to display at a time * @return the MenuScroller * @throws IllegalArgumentException if scrollCount is 0 or negative */ public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount) { return new MenuScroller(menu, scrollCount); } /** * Registers a menu to be scrolled, with the specified number of items to * display at a time and the specified scrolling interval. *
* @param menu the menu * @param scrollCount the number of items to be displayed at a time * @param interval the scroll interval, in milliseconds * @return the MenuScroller * @throws IllegalArgumentException if scrollCount or interval is 0 or negative */ public static MenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval) { return new MenuScroller(menu, scrollCount, interval); } /** * Registers a popup menu to be scrolled, with the specified number of items to * display at a time and the specified scrolling interval. *
* @param menu the popup menu * @param scrollCount the number of items to be displayed at a time * @param interval the scroll interval, in milliseconds * @return the MenuScroller * @throws IllegalArgumentException if scrollCount or interval is 0 or negative */ public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount, int interval) { return new MenuScroller(menu, scrollCount, interval); } /** * Registers a menu to be scrolled, with the specified number of items * to display in the scrolling region, the specified scrolling interval, * and the specified numbers of items fixed at the top and bottom of the * menu. *
* @param menu the menu * @param scrollCount the number of items to display in the scrolling portion * @param interval the scroll interval, in milliseconds * @param topFixedCount the number of items to fix at the top. May be 0. * @param bottomFixedCount the number of items to fix at the bottom. May be 0 * @throws IllegalArgumentException if scrollCount or interval is 0 or * negative or if topFixedCount or bottomFixedCount is negative * @return the MenuScroller */ public static MenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval, int topFixedCount, int bottomFixedCount) { return new MenuScroller(menu, scrollCount, interval, topFixedCount, bottomFixedCount); } /** * Registers a popup menu to be scrolled, with the specified number of items * to display in the scrolling region, the specified scrolling interval, * and the specified numbers of items fixed at the top and bottom of the * popup menu. *
* @param menu the popup menu
* @param scrollCount the number of items to display in the scrolling portion
* @param interval the scroll interval, in milliseconds
* @param topFixedCount the number of items to fix at the top. May be 0
* @param bottomFixedCount the number of items to fix at the bottom. May be 0
* @throws IllegalArgumentException if scrollCount or interval is 0 or
* negative or if topFixedCount or bottomFixedCount is negative
* @return the MenuScroller
*/
public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount,
int interval, int topFixedCount, int bottomFixedCount) {
return new MenuScroller(menu, scrollCount, interval,
topFixedCount, bottomFixedCount);
}
/**
* Constructs a MenuScroller
that scrolls a menu with the
* default number of items to display at a time, and default scrolling
* interval.
*
* @param menu the menu
*/
public MenuScroller(JMenu menu) {
this(menu, 15);
}
/**
* Constructs a MenuScroller
that scrolls a popup menu with the
* default number of items to display at a time, and default scrolling
* interval.
*
* @param menu the popup menu
*/
public MenuScroller(JPopupMenu menu) {
this(menu, 15);
}
/**
* Constructs a MenuScroller
that scrolls a menu with the
* specified number of items to display at a time, and default scrolling
* interval.
*
* @param menu the menu
* @param scrollCount the number of items to display at a time
* @throws IllegalArgumentException if scrollCount is 0 or negative
*/
public MenuScroller(JMenu menu, int scrollCount) {
this(menu, scrollCount, 125);
}
/**
* Constructs a MenuScroller
that scrolls a popup menu with the
* specified number of items to display at a time, and default scrolling
* interval.
*
* @param menu the popup menu
* @param scrollCount the number of items to display at a time
* @throws IllegalArgumentException if scrollCount is 0 or negative
*/
public MenuScroller(JPopupMenu menu, int scrollCount) {
this(menu, scrollCount, 125);
}
/**
* Constructs a MenuScroller
that scrolls a menu with the
* specified number of items to display at a time, and specified scrolling
* interval.
*
* @param menu the menu
* @param scrollCount the number of items to display at a time
* @param interval the scroll interval, in milliseconds
* @throws IllegalArgumentException if scrollCount or interval is 0 or negative
*/
public MenuScroller(JMenu menu, int scrollCount, int interval) {
this(menu, scrollCount, interval, 0, 0);
}
/**
* Constructs a MenuScroller
that scrolls a popup menu with the
* specified number of items to display at a time, and specified scrolling
* interval.
*
* @param menu the popup menu
* @param scrollCount the number of items to display at a time
* @param interval the scroll interval, in milliseconds
* @throws IllegalArgumentException if scrollCount or interval is 0 or negative
*/
public MenuScroller(JPopupMenu menu, int scrollCount, int interval) {
this(menu, scrollCount, interval, 0, 0);
}
/**
* Constructs a MenuScroller
that scrolls a menu with the
* specified number of items to display in the scrolling region, the
* specified scrolling interval, and the specified numbers of items fixed at
* the top and bottom of the menu.
*
* @param menu the menu
* @param scrollCount the number of items to display in the scrolling portion
* @param interval the scroll interval, in milliseconds
* @param topFixedCount the number of items to fix at the top. May be 0
* @param bottomFixedCount the number of items to fix at the bottom. May be 0
* @throws IllegalArgumentException if scrollCount or interval is 0 or
* negative or if topFixedCount or bottomFixedCount is negative
*/
public MenuScroller(JMenu menu, int scrollCount, int interval,
int topFixedCount, int bottomFixedCount) {
setValues(scrollCount, interval, topFixedCount, bottomFixedCount);
this.menu = menu;
menu.addMenuListener(menuListener);
}
/**
* Constructs a MenuScroller
that scrolls a popup menu with the
* specified number of items to display in the scrolling region, the
* specified scrolling interval, and the specified numbers of items fixed at
* the top and bottom of the popup menu.
*
* @param menu the popup menu * @param scrollCount the number of items to display in the scrolling portion * @param interval the scroll interval, in milliseconds * @param topFixedCount the number of items to fix at the top. May be 0 * @param bottomFixedCount the number of items to fix at the bottom. May be 0 * @throws IllegalArgumentException if scrollCount or interval is 0 or * negative or if topFixedCount or bottomFixedCount is negative */ public MenuScroller(JPopupMenu menu, int scrollCount, int interval, int topFixedCount, int bottomFixedCount) { setValues(scrollCount, interval, topFixedCount, bottomFixedCount); this.popupMenu = menu; menu.addPopupMenuListener(menuListener); } private void setValues(int scrollCount, int interval, int topFixedCount, int bottomFixedCount) { if (scrollCount <= 0 || interval <= 0) { throw new IllegalArgumentException( "scrollCount and interval must be greater than 0"); } if (topFixedCount < 0 || bottomFixedCount < 0) { throw new IllegalArgumentException( "topFixedCount and bottomFixedCount cannot be negative"); } upItem = new MenuScrollItem(MenuIcon.UP, -1); downItem = new MenuScrollItem(MenuIcon.DOWN, +1); setScrollCount(scrollCount); setInterval(interval); setTopFixedCount(topFixedCount); setBottomFixedCount(bottomFixedCount); } /** * 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 in the scrolling portion of the menu. *
* @return the number of items to display at a time */ public int getscrollCount() { return scrollCount; } /** * Sets the number of items in the scrolling portion of the menu. *
* @param scrollCount the number of items to display at a time * @throws IllegalArgumentException if scrollCount is 0 or negative */ public void setScrollCount(int scrollCount) { if (scrollCount <= 0) { throw new IllegalArgumentException( "scrollCount must be greater than 0"); } this.scrollCount = scrollCount; if (menu != null) { menu.doClick(); } MenuSelectionManager.defaultManager().clearSelectedPath(); } /** * 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) { firstIndex = Math.max(firstIndex, topFixedCount); this.topFixedCount = topFixedCount; } /** * Returns the number of items fixed at the bottom of the menu or popup menu. *
* @return the number of items */ public int getBottomFixedCount() { return bottomFixedCount; } /** * Sets the number of items to fix at the bottom of the menu or popup menu. *
* @param bottomFixedCount the number of items
*/
public void setBottomFixedCount(int bottomFixedCount) {
this.bottomFixedCount = bottomFixedCount;
}
/**
* Returns whether to show seperators when scrolling is required.
*
* @return Whether to show seperators when scrolling is required
*/
public boolean getShowSeperators() {
return showSeperators;
}
/**
* Sets whether we show seperators when a menu requires scrolling.
*
* @param showSeperators true to add seperators
*/
public void setShowSeperators(boolean showSeperators) {
this.showSeperators = showSeperators;
}
/**
* Removes this MenuScroller from the associated menu and restores the
* default behavior of the menu.
*/
public void dispose() {
if (menu != null) {
menu.removeMenuListener(menuListener);
menu = null;
}
if (popupMenu != null) {
popupMenu.removePopupMenuListener(menuListener);
popupMenu = null;
}
}
/**
* Ensures that the dispose
method of this MenuScroller is
* called when there are no more refrences to it.
*
* @exception Throwable if an error occurs. * @see MenuScroller#dispose() */ @Override public void finalize() throws Throwable { dispose(); } private void setMenuItems() { if (menu != null) { menuItems = menu.getMenuComponents(); } if (popupMenu != null) { menuItems = popupMenu.getComponents(); } if (menuItems.length > topFixedCount + scrollCount + bottomFixedCount) { refreshMenu(); } } private void restoreMenuItems() { JComponent container = menu == null ? popupMenu : menu; container.removeAll(); for (Component component : menuItems) { container.add(component); } } private void refreshMenu() { firstIndex = Math.max(topFixedCount, firstIndex); firstIndex = Math.min(menuItems.length - bottomFixedCount - scrollCount, firstIndex); upItem.setEnabled(firstIndex > topFixedCount); downItem.setEnabled( firstIndex + scrollCount < menuItems.length - bottomFixedCount); JComponent container = menu == null ? popupMenu : menu; container.removeAll(); for (int i = 0; i < topFixedCount; i++) { container.add(menuItems[i]); } if (topFixedCount > 0 && showSeperators) { container.add(new JSeparator()); } container.add(upItem); for (int i = firstIndex; i < scrollCount + firstIndex; i++) { container.add(menuItems[i]); } container.add(downItem); if (bottomFixedCount > 0 && showSeperators) { container.add(new JSeparator()); } for (int i = menuItems.length - bottomFixedCount; i < menuItems.length; i++) { container.add(menuItems[i]); } upItem.getParent().validate(); } private class MenuScrollItem extends JMenuItem implements MouseListener, ChangeListener { private static final long serialVersionUID = -9164893168723608060L; private MenuScrollTimer timer; private MouseListener[] mouseListeners; public MenuScrollItem(MenuIcon icon, int increment) { setIcon(icon); setDisabledIcon(icon); timer = new MenuScrollTimer(increment, interval); mouseListeners = getMouseListeners(); addMouseListener(this); addChangeListener(this); } public void setInterval(int interval) { timer.setDelay(interval); } // MouseListener methods @Override public void mouseClicked(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { } @Override public void mouseReleased(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { for (MouseListener mouseListener : mouseListeners) { mouseListener.mouseEntered(e); } } @Override public void mouseExited(MouseEvent e) { for (MouseListener mouseListener : mouseListeners) { mouseListener.mouseExited(e); } } // ChangeListener method @Override public void stateChanged(ChangeEvent e) { if (isArmed() && !timer.isRunning()) { timer.start(); } if (!isArmed() && timer.isRunning()) { timer.stop(); } } } private class MenuScrollTimer extends Timer { private static final long serialVersionUID = 1329095900644142841L; public MenuScrollTimer(final int increment, int interval) { super(interval, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { firstIndex += increment; refreshMenu(); } }); } } private class MenuScrollListener implements MenuListener, PopupMenuListener { // MenuListener methods @Override public void menuSelected(MenuEvent e) { setMenuItems(); } @Override public void menuDeselected(MenuEvent e) { restoreMenuItems(); } @Override public void menuCanceled(MenuEvent e) { restoreMenuItems(); } // PopupMenuListener methods @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { setMenuItems(); } @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { restoreMenuItems(); } @Override public void popupMenuCanceled(PopupMenuEvent e) { restoreMenuItems(); } } private static enum MenuIcon implements Icon { UP(9, 1, 9), DOWN(1, 9, 1); final int[] xPoints = {1, 5, 9}; 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 10; } } }