Ticket #5638: 5638.patch

File 5638.patch, 22.5 KB (added by simon04, 14 years ago)
  • src/org/openstreetmap/josm/gui/MainMenu.java

    diff --git a/src/org/openstreetmap/josm/gui/MainMenu.java b/src/org/openstreetmap/josm/gui/MainMenu.java
    index cce2765..d0291cf 100644
    a b  
    11// License: GPL. See LICENSE file for details.
    22package org.openstreetmap.josm.gui;
    33
     4import java.awt.Toolkit;
    45import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
    56import static org.openstreetmap.josm.tools.I18n.marktr;
    67import static org.openstreetmap.josm.tools.I18n.tr;
    public class MainMenu extends JMenuBar {  
    221222    }
    222223
    223224    public JMenu addMenu(String name, int mnemonicKey, int position, String relativeHelpTopic) {
    224         return addMenu(new JMenu(tr(name)), name, mnemonicKey, position, relativeHelpTopic);
     225        final JMenu menu = new JMenu(tr(name));
     226        int screenHeight = Toolkit.getDefaultToolkit().getScreenSize().height;
     227        int menuItemHeight = new JMenu().add(newAction).getPreferredSize().height;
     228        MenuScroller.setScrollerFor(menu, screenHeight / menuItemHeight);
     229        return addMenu(menu, name, mnemonicKey, position, relativeHelpTopic);
    225230    }
    226231
    227232    public JMenu addMenu(JMenu menu, String name, int mnemonicKey, int position, String relativeHelpTopic) {
  • new file src/org/openstreetmap/josm/gui/MenuScroller.java

    diff --git a/src/org/openstreetmap/josm/gui/MenuScroller.java b/src/org/openstreetmap/josm/gui/MenuScroller.java
    new file mode 100644
    index 0000000..1a2600c
    - +  
     1/**
     2 * @(#)MenuScroller.java        1.4.0 14/09/10
     3 */
     4package org.openstreetmap.josm.gui;
     5
     6import java.awt.Color;
     7import java.awt.Component;
     8import java.awt.Dimension;
     9import java.awt.Graphics;
     10import java.awt.event.ActionEvent;
     11import java.awt.event.ActionListener;
     12import java.awt.event.MouseWheelEvent;
     13import java.awt.event.MouseWheelListener;
     14import java.beans.PropertyChangeEvent;
     15import java.beans.PropertyChangeListener;
     16import javax.swing.Icon;
     17import javax.swing.JComponent;
     18import javax.swing.JMenu;
     19import javax.swing.JMenuItem;
     20import javax.swing.JPopupMenu;
     21import javax.swing.JSeparator;
     22import javax.swing.MenuSelectionManager;
     23import javax.swing.Timer;
     24import javax.swing.event.ChangeEvent;
     25import javax.swing.event.ChangeListener;
     26import javax.swing.event.PopupMenuEvent;
     27import javax.swing.event.PopupMenuListener;
     28
     29/**
     30 * A class that provides scrolling capabilities to a long menu dropdown or
     31 * popup menu.  A number of items can optionally be frozen at the top and/or
     32 * bottom of the menu.
     33 * <P>
     34 * <B>Implementation note:</B>  The default number of items to display
     35 * at a time is 15, and the default scrolling interval is 125 milliseconds.
     36 * <P>
     37 * @author Darryl, http://tips4java.wordpress.com/2009/02/01/menu-scroller/
     38 */
     39public class MenuScroller {
     40
     41    //private JMenu menu;
     42    private JPopupMenu menu;
     43    private Component[] menuItems;
     44    private MenuScrollItem upItem;
     45    private MenuScrollItem downItem;
     46    private final MenuScrollListener menuListener = new MenuScrollListener();
     47    private int scrollCount;
     48    private int interval;
     49    private int topFixedCount;
     50    private int bottomFixedCount;
     51    private int firstIndex = 0;
     52    private int keepVisibleIndex = -1;
     53
     54    /**
     55     * Registers a menu to be scrolled with the default number of items to
     56     * display at a time and the default scrolling interval.
     57     *
     58     * @param menu the menu
     59     * @return the MenuScroller
     60     */
     61    public static MenuScroller setScrollerFor(JMenu menu) {
     62        return new MenuScroller(menu);
     63    }
     64
     65    /**
     66     * Registers a popup menu to be scrolled with the default number of items to
     67     * display at a time and the default scrolling interval.
     68     *
     69     * @param menu the popup menu
     70     * @return the MenuScroller
     71     */
     72    public static MenuScroller setScrollerFor(JPopupMenu menu) {
     73        return new MenuScroller(menu);
     74    }
     75
     76    /**
     77     * Registers a menu to be scrolled with the default number of items to
     78     * display at a time and the specified scrolling interval.
     79     *
     80     * @param menu the menu
     81     * @param scrollCount the number of items to display at a time
     82     * @return the MenuScroller
     83     * @throws IllegalArgumentException if scrollCount is 0 or negative
     84     */
     85    public static MenuScroller setScrollerFor(JMenu menu, int scrollCount) {
     86        return new MenuScroller(menu, scrollCount);
     87    }
     88
     89    /**
     90     * Registers a popup menu to be scrolled with the default number of items to
     91     * display at a time and the specified scrolling interval.
     92     *
     93     * @param menu the popup menu
     94     * @param scrollCount the number of items to display at a time
     95     * @return the MenuScroller
     96     * @throws IllegalArgumentException if scrollCount is 0 or negative
     97     */
     98    public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount) {
     99        return new MenuScroller(menu, scrollCount);
     100    }
     101
     102    /**
     103     * Registers a menu to be scrolled, with the specified number of items to
     104     * display at a time and the specified scrolling interval.
     105     *
     106     * @param menu the menu
     107     * @param scrollCount the number of items to be displayed at a time
     108     * @param interval the scroll interval, in milliseconds
     109     * @return the MenuScroller
     110     * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
     111     */
     112    public static MenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval) {
     113        return new MenuScroller(menu, scrollCount, interval);
     114    }
     115
     116    /**
     117     * Registers a popup menu to be scrolled, with the specified number of items to
     118     * display at a time and the specified scrolling interval.
     119     *
     120     * @param menu the popup menu
     121     * @param scrollCount the number of items to be displayed at a time
     122     * @param interval the scroll interval, in milliseconds
     123     * @return the MenuScroller
     124     * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
     125     */
     126    public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount, int interval) {
     127        return new MenuScroller(menu, scrollCount, interval);
     128    }
     129
     130    /**
     131     * Registers a menu to be scrolled, with the specified number of items
     132     * to display in the scrolling region, the specified scrolling interval,
     133     * and the specified numbers of items fixed at the top and bottom of the
     134     * menu.
     135     *
     136     * @param menu the menu
     137     * @param scrollCount the number of items to display in the scrolling portion
     138     * @param interval the scroll interval, in milliseconds
     139     * @param topFixedCount the number of items to fix at the top.  May be 0.
     140     * @param bottomFixedCount the number of items to fix at the bottom. May be 0
     141     * @throws IllegalArgumentException if scrollCount or interval is 0 or
     142     * negative or if topFixedCount or bottomFixedCount is negative
     143     * @return the MenuScroller
     144     */
     145    public static MenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval,
     146            int topFixedCount, int bottomFixedCount) {
     147        return new MenuScroller(menu, scrollCount, interval,
     148                topFixedCount, bottomFixedCount);
     149    }
     150
     151    /**
     152     * Registers a popup menu to be scrolled, with the specified number of items
     153     * to display in the scrolling region, the specified scrolling interval,
     154     * and the specified numbers of items fixed at the top and bottom of the
     155     * popup menu.
     156     *
     157     * @param menu the popup menu
     158     * @param scrollCount the number of items to display in the scrolling portion
     159     * @param interval the scroll interval, in milliseconds
     160     * @param topFixedCount the number of items to fix at the top.  May be 0
     161     * @param bottomFixedCount the number of items to fix at the bottom.  May be 0
     162     * @throws IllegalArgumentException if scrollCount or interval is 0 or
     163     * negative or if topFixedCount or bottomFixedCount is negative
     164     * @return the MenuScroller
     165     */
     166    public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount, int interval,
     167            int topFixedCount, int bottomFixedCount) {
     168        return new MenuScroller(menu, scrollCount, interval,
     169                topFixedCount, bottomFixedCount);
     170    }
     171
     172    /**
     173     * Constructs a <code>MenuScroller</code> that scrolls a menu with the
     174     * default number of items to display at a time, and default scrolling
     175     * interval.
     176     *
     177     * @param menu the menu
     178     */
     179    public MenuScroller(JMenu menu) {
     180        this(menu, 15);
     181    }
     182
     183    /**
     184     * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
     185     * default number of items to display at a time, and default scrolling
     186     * interval.
     187     *
     188     * @param menu the popup menu
     189     */
     190    public MenuScroller(JPopupMenu menu) {
     191        this(menu, 15);
     192    }
     193
     194    /**
     195     * Constructs a <code>MenuScroller</code> that scrolls a menu with the
     196     * specified number of items to display at a time, and default scrolling
     197     * interval.
     198     *
     199     * @param menu the menu
     200     * @param scrollCount the number of items to display at a time
     201     * @throws IllegalArgumentException if scrollCount is 0 or negative
     202     */
     203    public MenuScroller(JMenu menu, int scrollCount) {
     204        this(menu, scrollCount, 150);
     205    }
     206
     207    /**
     208     * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
     209     * specified number of items to display at a time, and default scrolling
     210     * interval.
     211     *
     212     * @param menu the popup menu
     213     * @param scrollCount the number of items to display at a time
     214     * @throws IllegalArgumentException if scrollCount is 0 or negative
     215     */
     216    public MenuScroller(JPopupMenu menu, int scrollCount) {
     217        this(menu, scrollCount, 150);
     218    }
     219
     220    /**
     221     * Constructs a <code>MenuScroller</code> that scrolls a menu with the
     222     * specified number of items to display at a time, and specified scrolling
     223     * interval.
     224     *
     225     * @param menu the menu
     226     * @param scrollCount the number of items to display at a time
     227     * @param interval the scroll interval, in milliseconds
     228     * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
     229     */
     230    public MenuScroller(JMenu menu, int scrollCount, int interval) {
     231        this(menu, scrollCount, interval, 0, 0);
     232    }
     233
     234    /**
     235     * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
     236     * specified number of items to display at a time, and specified scrolling
     237     * interval.
     238     *
     239     * @param menu the popup menu
     240     * @param scrollCount the number of items to display at a time
     241     * @param interval the scroll interval, in milliseconds
     242     * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
     243     */
     244    public MenuScroller(JPopupMenu menu, int scrollCount, int interval) {
     245        this(menu, scrollCount, interval, 0, 0);
     246    }
     247
     248    /**
     249     * Constructs a <code>MenuScroller</code> that scrolls a menu with the
     250     * specified number of items to display in the scrolling region, the
     251     * specified scrolling interval, and the specified numbers of items fixed at
     252     * the top and bottom of the menu.
     253     *
     254     * @param menu the menu
     255     * @param scrollCount the number of items to display in the scrolling portion
     256     * @param interval the scroll interval, in milliseconds
     257     * @param topFixedCount the number of items to fix at the top.  May be 0
     258     * @param bottomFixedCount the number of items to fix at the bottom.  May be 0
     259     * @throws IllegalArgumentException if scrollCount or interval is 0 or
     260     * negative or if topFixedCount or bottomFixedCount is negative
     261     */
     262    public MenuScroller(JMenu menu, int scrollCount, int interval,
     263            int topFixedCount, int bottomFixedCount) {
     264        this(menu.getPopupMenu(), scrollCount, interval, topFixedCount, bottomFixedCount);
     265    }
     266
     267    /**
     268     * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
     269     * specified number of items to display in the scrolling region, the
     270     * specified scrolling interval, and the specified numbers of items fixed at
     271     * the top and bottom of the popup menu.
     272     *
     273     * @param menu the popup menu
     274     * @param scrollCount the number of items to display in the scrolling portion
     275     * @param interval the scroll interval, in milliseconds
     276     * @param topFixedCount the number of items to fix at the top.  May be 0
     277     * @param bottomFixedCount the number of items to fix at the bottom.  May be 0
     278     * @throws IllegalArgumentException if scrollCount or interval is 0 or
     279     * negative or if topFixedCount or bottomFixedCount is negative
     280     */
     281    public MenuScroller(JPopupMenu menu, int scrollCount, int interval,
     282            int topFixedCount, int bottomFixedCount) {
     283        if (scrollCount <= 0 || interval <= 0) {
     284            throw new IllegalArgumentException("scrollCount and interval must be greater than 0");
     285        }
     286        if (topFixedCount < 0 || bottomFixedCount < 0) {
     287            throw new IllegalArgumentException("topFixedCount and bottomFixedCount cannot be negative");
     288        }
     289
     290        upItem = new MenuScrollItem(MenuIcon.UP, -1);
     291        downItem = new MenuScrollItem(MenuIcon.DOWN, +1);
     292        setScrollCount(scrollCount);
     293        setInterval(interval);
     294        setTopFixedCount(topFixedCount);
     295        setBottomFixedCount(bottomFixedCount);
     296
     297        this.menu = menu;
     298        menu.addPopupMenuListener(menuListener);
     299    }
     300
     301    /**
     302     * Returns the scroll interval in milliseconds
     303     *
     304     * @return the scroll interval in milliseconds
     305     */
     306    public int getInterval() {
     307        return interval;
     308    }
     309
     310    /**
     311     * Sets the scroll interval in milliseconds
     312     *
     313     * @param interval the scroll interval in milliseconds
     314     * @throws IllegalArgumentException if interval is 0 or negative
     315     */
     316    public void setInterval(int interval) {
     317        if (interval <= 0) {
     318            throw new IllegalArgumentException("interval must be greater than 0");
     319        }
     320        upItem.setInterval(interval);
     321        downItem.setInterval(interval);
     322        this.interval = interval;
     323    }
     324
     325    /**
     326     * Returns the number of items in the scrolling portion of the menu.
     327     *
     328     * @return the number of items to display at a time
     329     */
     330    public int getscrollCount() {
     331        return scrollCount;
     332    }
     333
     334    /**
     335     * Sets the number of items in the scrolling portion of the menu.
     336     *
     337     * @param scrollCount the number of items to display at a time
     338     * @throws IllegalArgumentException if scrollCount is 0 or negative
     339     */
     340    public void setScrollCount(int scrollCount) {
     341        if (scrollCount <= 0) {
     342            throw new IllegalArgumentException("scrollCount must be greater than 0");
     343        }
     344        this.scrollCount = scrollCount;
     345        MenuSelectionManager.defaultManager().clearSelectedPath();
     346    }
     347
     348    /**
     349     * Returns the number of items fixed at the top of the menu or popup menu.
     350     *
     351     * @return the number of items
     352     */
     353    public int getTopFixedCount() {
     354        return topFixedCount;
     355    }
     356
     357    /**
     358     * Sets the number of items to fix at the top of the menu or popup menu.
     359     *
     360     * @param topFixedCount the number of items
     361     */
     362    public void setTopFixedCount(int topFixedCount) {
     363        if (firstIndex <= topFixedCount) {
     364            firstIndex = topFixedCount;
     365        } else {
     366            firstIndex += (topFixedCount - this.topFixedCount);
     367        }
     368        this.topFixedCount = topFixedCount;
     369    }
     370
     371    /**
     372     * Returns the number of items fixed at the bottom of the menu or popup menu.
     373     *
     374     * @return the number of items
     375     */
     376    public int getBottomFixedCount() {
     377        return bottomFixedCount;
     378    }
     379
     380    /**
     381     * Sets the number of items to fix at the bottom of the menu or popup menu.
     382     *
     383     * @param bottomFixedCount the number of items
     384     */
     385    public void setBottomFixedCount(int bottomFixedCount) {
     386        this.bottomFixedCount = bottomFixedCount;
     387    }
     388
     389    /**
     390     * Scrolls the specified item into view each time the menu is opened.  Call this method with
     391     * <code>null</code> to restore the default behavior, which is to show the menu as it last
     392     * appeared.
     393     *
     394     * @param item the item to keep visible
     395     * @see #keepVisible(int)
     396     */
     397    public void keepVisible(JMenuItem item) {
     398        if (item == null) {
     399            keepVisibleIndex = -1;
     400        } else {
     401            int index = menu.getComponentIndex(item);
     402            keepVisibleIndex = index;
     403        }
     404    }
     405
     406    /**
     407     * Scrolls the item at the specified index into view each time the menu is opened.  Call this
     408     * method with <code>-1</code> to restore the default behavior, which is to show the menu as
     409     * it last appeared.
     410     *
     411     * @param index the index of the item to keep visible
     412     * @see #keepVisible(javax.swing.JMenuItem)
     413     */
     414    public void keepVisible(int index) {
     415        keepVisibleIndex = index;
     416    }
     417
     418    /**
     419     * Removes this MenuScroller from the associated menu and restores the
     420     * default behavior of the menu.
     421     */
     422    public void dispose() {
     423        if (menu != null) {
     424            menu.removePopupMenuListener(menuListener);
     425            menu = null;
     426        }
     427    }
     428
     429    /**
     430     * Ensures that the <code>dispose</code> method of this MenuScroller is
     431     * called when there are no more refrences to it.
     432     *
     433     * @exception  Throwable if an error occurs.
     434     * @see MenuScroller#dispose()
     435     */
     436    @Override
     437    public void finalize() throws Throwable {
     438        dispose();
     439    }
     440
     441    private void refreshMenu() {
     442        if (menuItems != null && menuItems.length > 0) {
     443            firstIndex = Math.max(topFixedCount, firstIndex);
     444            firstIndex = Math.min(menuItems.length - bottomFixedCount - scrollCount, firstIndex);
     445
     446            upItem.setEnabled(firstIndex > topFixedCount);
     447            downItem.setEnabled(firstIndex + scrollCount < menuItems.length - bottomFixedCount);
     448
     449            menu.removeAll();
     450            for (int i = 0; i < topFixedCount; i++) {
     451                menu.add(menuItems[i]);
     452            }
     453            if (topFixedCount > 0) {
     454                menu.add(new JSeparator());
     455            }
     456
     457            menu.add(upItem);
     458            for (int i = firstIndex; i < scrollCount + firstIndex; i++) {
     459                menu.add(menuItems[i]);
     460            }
     461            menu.add(downItem);
     462
     463            if (bottomFixedCount > 0) {
     464                menu.add(new JSeparator());
     465            }
     466            for (int i = menuItems.length - bottomFixedCount; i < menuItems.length; i++) {
     467                menu.add(menuItems[i]);
     468            }
     469
     470            int preferredWidth = 0;
     471            for (Component item : menuItems) {
     472                preferredWidth = Math.max(preferredWidth, item.getPreferredSize().width);
     473            }
     474            menu.setPreferredSize(new Dimension(preferredWidth, menu.getPreferredSize().height));
     475
     476            JComponent parent = (JComponent) upItem.getParent();
     477            parent.revalidate();
     478            parent.repaint();
     479        }
     480    }
     481
     482    private class MenuScrollListener implements PopupMenuListener {
     483
     484        @Override
     485        public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
     486            setMenuItems();
     487        }
     488
     489        @Override
     490        public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
     491            restoreMenuItems();
     492        }
     493
     494        @Override
     495        public void popupMenuCanceled(PopupMenuEvent e) {
     496            restoreMenuItems();
     497        }
     498
     499        private void setMenuItems() {
     500            menuItems = menu.getComponents();
     501            if (keepVisibleIndex >= topFixedCount
     502                    && keepVisibleIndex <= menuItems.length - bottomFixedCount
     503                    && (keepVisibleIndex > firstIndex + scrollCount
     504                    || keepVisibleIndex < firstIndex)) {
     505                firstIndex = Math.min(firstIndex, keepVisibleIndex);
     506                firstIndex = Math.max(firstIndex, keepVisibleIndex - scrollCount + 1);
     507            }
     508            if (menuItems.length > topFixedCount + scrollCount + bottomFixedCount) {
     509                refreshMenu();
     510            }
     511        }
     512
     513        private void restoreMenuItems() {
     514            menu.removeAll();
     515            for (Component component : menuItems) {
     516                menu.add(component);
     517            }
     518        }
     519    }
     520
     521    private class MenuScrollTimer extends Timer {
     522
     523        public MenuScrollTimer(final int increment, int interval) {
     524            super(interval, new ActionListener() {
     525
     526                @Override
     527                public void actionPerformed(ActionEvent e) {
     528                    firstIndex += increment;
     529                    refreshMenu();
     530                }
     531            });
     532        }
     533    }
     534
     535    private class MenuScrollItem extends JMenuItem
     536            implements ChangeListener {
     537
     538        private MenuScrollTimer timer;
     539
     540        public MenuScrollItem(MenuIcon icon, int increment) {
     541            setIcon(icon);
     542            setDisabledIcon(icon);
     543            timer = new MenuScrollTimer(increment, interval);
     544            addChangeListener(this);
     545        }
     546
     547        public void setInterval(int interval) {
     548            timer.setDelay(interval);
     549        }
     550
     551        @Override
     552        public void stateChanged(ChangeEvent e) {
     553            if (isArmed() && !timer.isRunning()) {
     554                timer.start();
     555            }
     556            if (!isArmed() && timer.isRunning()) {
     557                timer.stop();
     558            }
     559        }
     560    }
     561
     562    private static enum MenuIcon implements Icon {
     563
     564        UP(9, 1, 9),
     565        DOWN(1, 9, 1);
     566        final int[] xPoints = {1, 5, 9};
     567        final int[] yPoints;
     568
     569        MenuIcon(int... yPoints) {
     570            this.yPoints = yPoints;
     571        }
     572
     573        @Override
     574        public void paintIcon(Component c, Graphics g, int x, int y) {
     575            Dimension size = c.getSize();
     576            Graphics g2 = g.create(size.width / 2 - 5, size.height / 2 - 5, 10, 10);
     577            g2.setColor(Color.GRAY);
     578            g2.drawPolygon(xPoints, yPoints, 3);
     579            if (c.isEnabled()) {
     580                g2.setColor(Color.BLACK);
     581                g2.fillPolygon(xPoints, yPoints, 3);
     582            }
     583            g2.dispose();
     584        }
     585
     586        @Override
     587        public int getIconWidth() {
     588            return 0;
     589        }
     590
     591        @Override
     592        public int getIconHeight() {
     593            return 10;
     594        }
     595    }
     596}