From e1856b373c623106d5a660b41d87641f07754191 Mon Sep 17 00:00:00 2001 From: Federico Fissore Date: Wed, 16 Jan 2013 17:19:04 +0100 Subject: [PATCH] scrollable menu --- app/src/processing/app/Editor.java | 2 + .../processing/app/tools/MenuScroller.java | 633 ++++++++++++++++++ 2 files changed, 635 insertions(+) create mode 100644 app/src/processing/app/tools/MenuScroller.java diff --git a/app/src/processing/app/Editor.java b/app/src/processing/app/Editor.java index f83bd0e4f..1f55fa733 100644 --- a/app/src/processing/app/Editor.java +++ b/app/src/processing/app/Editor.java @@ -508,12 +508,14 @@ public class Editor extends JFrame implements RunnerListener { if (sketchbookMenu == null) { sketchbookMenu = new JMenu(_("Sketchbook")); + MenuScroller.setScrollerFor(sketchbookMenu); base.rebuildSketchbookMenu(sketchbookMenu); } fileMenu.add(sketchbookMenu); if (examplesMenu == null) { examplesMenu = new JMenu(_("Examples")); + MenuScroller.setScrollerFor(examplesMenu); base.rebuildExamplesMenu(examplesMenu); } fileMenu.add(examplesMenu); diff --git a/app/src/processing/app/tools/MenuScroller.java b/app/src/processing/app/tools/MenuScroller.java new file mode 100644 index 000000000..3a3d98b16 --- /dev/null +++ b/app/src/processing/app/tools/MenuScroller.java @@ -0,0 +1,633 @@ +/** + * @(#)MenuScroller.java 1.5.0 04/02/12 + */ +package processing.app.tools; + +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 javax.swing.Icon; +import javax.swing.JComponent; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.MenuSelectionManager; +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 javax.swing.plaf.*; + +/** + * 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. + *

+ * + * @version 1.5.0 04/05/12 + * @author Darryl + */ +public class MenuScroller { + + //private JMenu menu; + private JPopupMenu menu; + 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 int keepVisibleIndex = -1; + + private static int getMaximumItems(JPopupMenu menu) { + JMenuItem test = new JMenuItem("test"); + ButtonUI ui = test.getUI(); + Dimension d = ui.getPreferredSize(test); + double item_height = d.getHeight(); + //System.out.println("JMenuItem Height " + item_height); + JMenuItem up = new JMenuItem(MenuIcon.UP); + ui = up.getUI(); + d = ui.getPreferredSize(up); + double icon_height = d.getHeight(); + //System.out.println("icon item height " + icon_height); + double menu_border_height = 8.0; // kludge - how to detect this? + double screen_height = java.awt.Toolkit.getDefaultToolkit().getScreenSize().getHeight(); + //System.out.println("screen height " + screen_height); + int n = (int)((screen_height - icon_height * 2 - menu_border_height) / item_height); + //System.out.println("max items " + n); + return n; + } + + /** + * 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, -1); + } + + /** + * 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, -1); + } + + /** + * 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, 150); + } + + /** + * 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, 150); + } + + /** + * 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) { + this(menu.getPopupMenu(), scrollCount, interval, topFixedCount, bottomFixedCount); + } + + /** + * 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) { + + if(scrollCount == -1) + scrollCount = getMaximumItems(menu)-topFixedCount-bottomFixedCount; // Autosize + + if(interval == -1) + interval = 150; // Default value + + 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); + + this.menu = menu; + menu.addPopupMenuListener(menuListener); + } + + /** + * 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; + 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) { + if (firstIndex <= topFixedCount) { + firstIndex = topFixedCount; + } else { + firstIndex += (topFixedCount - this.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; + } + + /** + * Scrolls the specified item into view each time the menu is opened. Call this method with + * null to restore the default behavior, which is to show the menu as it last + * appeared. + * + * @param item the item to keep visible + * @see #keepVisible(int) + */ + public void keepVisible(JMenuItem item) { + if (item == null) { + keepVisibleIndex = -1; + } else { + int index = menu.getComponentIndex(item); + keepVisibleIndex = index; + } + } + + /** + * Scrolls the item at the specified index into view each time the menu is opened. Call this + * method with -1 to restore the default behavior, which is to show the menu as + * it last appeared. + * + * @param index the index of the item to keep visible + * @see #keepVisible(javax.swing.JMenuItem) + */ + public void keepVisible(int index) { + keepVisibleIndex = index; + } + + /** + * 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 = 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 refreshMenu() { + if (menuItems != null && menuItems.length > 0) { + firstIndex = Math.max(topFixedCount, firstIndex); + firstIndex = Math.min(menuItems.length - bottomFixedCount - scrollCount, firstIndex); + + upItem.setEnabled(firstIndex > topFixedCount); + downItem.setEnabled(firstIndex + scrollCount < menuItems.length - bottomFixedCount); + + 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); + + /*if (bottomFixedCount > 0) { + menu.addSeparator(); + }*/ + for (int i = menuItems.length - bottomFixedCount; i < menuItems.length; i++) { + menu.add(menuItems[i]); + } + + JComponent parent = (JComponent) upItem.getParent(); + parent.revalidate(); + parent.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(); + + // Hack for auto detect the topFixed total + /*int topFixedCountPrev = topFixedCount; + for(int i=menuItems.length-1;i>0;i--) + { + if(menuItems[i].getClass().getName().endsWith("Separator")) + { + System.out.println(i); + setTopFixedCount(i+1); + + if(topFixedCount!=topFixedCountPrev) + { + scrollCount = getMaximumItems()-topFixedCount; + System.out.println(getMaximumItems()-topFixedCount); + } + break; + } + }*/ + + if (keepVisibleIndex >= topFixedCount + && keepVisibleIndex <= menuItems.length - bottomFixedCount + && (keepVisibleIndex > firstIndex + scrollCount + || keepVisibleIndex < firstIndex)) { + firstIndex = Math.min(firstIndex, keepVisibleIndex); + firstIndex = Math.max(firstIndex, keepVisibleIndex - scrollCount + 1); + } + if (menuItems.length > topFixedCount + scrollCount + bottomFixedCount) { + refreshMenu(); + } + } + + private void restoreMenuItems() { + menu.removeAll(); + for (Component component : menuItems) { + menu.add(component); + } + } + } + + private class MenuScrollTimer extends Timer { + + public 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 MenuScrollTimer timer; + + public 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 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; + } + } +}