source: josm/trunk/src/org/openstreetmap/josm/gui/MenuScroller.java @ 5241

Revision 5088, 20.2 KB checked in by simon04, 2 months ago (diff)

fix #6895 - improve MapPaintMenu (add "Preference", keep menu open after clicking checkbox) - thanks: http://tips4java.wordpress.com/2010/09/12/keeping-menus-open/

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