001    /**
002     * @(#)MenuScroller.java        1.4.0 14/09/10
003     */
004    package org.openstreetmap.josm.gui;
005    
006    import java.awt.Color;
007    import java.awt.Component;
008    import java.awt.Dimension;
009    import java.awt.Graphics;
010    import java.awt.event.ActionEvent;
011    import java.awt.event.ActionListener;
012    import javax.swing.Icon;
013    import javax.swing.JComponent;
014    import javax.swing.JMenu;
015    import javax.swing.JMenuItem;
016    import javax.swing.JPopupMenu;
017    import javax.swing.JSeparator;
018    import javax.swing.MenuSelectionManager;
019    import javax.swing.Timer;
020    import javax.swing.event.ChangeEvent;
021    import javax.swing.event.ChangeListener;
022    import javax.swing.event.PopupMenuEvent;
023    import javax.swing.event.PopupMenuListener;
024    
025    /**
026     * A class that provides scrolling capabilities to a long menu dropdown or
027     * popup menu.  A number of items can optionally be frozen at the top and/or
028     * bottom of the menu.
029     * <P>
030     * <B>Implementation note:</B>  The default number of items to display
031     * at a time is 15, and the default scrolling interval is 125 milliseconds.
032     * <P>
033     * @author Darryl, http://tips4java.wordpress.com/2009/02/01/menu-scroller/
034     */
035    public class MenuScroller {
036    
037        //private JMenu menu;
038        private JPopupMenu menu;
039        private Component[] menuItems;
040        private MenuScrollItem upItem;
041        private MenuScrollItem downItem;
042        private final MenuScrollListener menuListener = new MenuScrollListener();
043        private int scrollCount;
044        private int interval;
045        private int topFixedCount;
046        private int bottomFixedCount;
047        private int firstIndex = 0;
048        private int keepVisibleIndex = -1;
049    
050        /**
051         * Registers a menu to be scrolled with the default number of items to
052         * display at a time and the default scrolling interval.
053         *
054         * @param menu the menu
055         * @return the MenuScroller
056         */
057        public static MenuScroller setScrollerFor(JMenu menu) {
058            return new MenuScroller(menu);
059        }
060    
061        /**
062         * Registers a popup menu to be scrolled with the default number of items to
063         * display at a time and the default scrolling interval.
064         *
065         * @param menu the popup menu
066         * @return the MenuScroller
067         */
068        public static MenuScroller setScrollerFor(JPopupMenu menu) {
069            return new MenuScroller(menu);
070        }
071    
072        /**
073         * Registers a menu to be scrolled with the default number of items to
074         * display at a time and the specified scrolling interval.
075         *
076         * @param menu the menu
077         * @param scrollCount the number of items to display at a time
078         * @return the MenuScroller
079         * @throws IllegalArgumentException if scrollCount is 0 or negative
080         */
081        public static MenuScroller setScrollerFor(JMenu menu, int scrollCount) {
082            return new MenuScroller(menu, scrollCount);
083        }
084    
085        /**
086         * Registers a popup menu to be scrolled with the default number of items to
087         * display at a time and the specified scrolling interval.
088         *
089         * @param menu the popup menu
090         * @param scrollCount the number of items to display at a time
091         * @return the MenuScroller
092         * @throws IllegalArgumentException if scrollCount is 0 or negative
093         */
094        public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount) {
095            return new MenuScroller(menu, scrollCount);
096        }
097    
098        /**
099         * 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    }