View Javadoc

1   package com.eviware.soapui.support.swing;
2   /***
3    * @(#)MenuScroller.java	1.4.1 2010-09-29
4    */
5   
6   import java.awt.Color;
7   import java.awt.Component;
8   import java.awt.Dimension;
9   import java.awt.Graphics;
10  import java.awt.event.ActionEvent;
11  import java.awt.event.ActionListener;
12  import javax.swing.Icon;
13  import javax.swing.JComponent;
14  import javax.swing.JMenu;
15  import javax.swing.JMenuItem;
16  import javax.swing.JPopupMenu;
17  import javax.swing.JSeparator;
18  import javax.swing.MenuSelectionManager;
19  import javax.swing.Timer;
20  import javax.swing.event.ChangeEvent;
21  import javax.swing.event.ChangeListener;
22  import javax.swing.event.PopupMenuEvent;
23  import 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
34   * @author Henrik Olsson
35   * 
36   * 2010-09-29, Henrik: Never show separators if rendered last in a scrolling list.  
37   */
38  public class MenuScroller {
39  
40    //private JMenu menu;
41    private JPopupMenu menu;
42    private Component[] menuItems;
43    private MenuScrollItem upItem;
44    private MenuScrollItem downItem;
45    private final MenuScrollListener menuListener = new MenuScrollListener();
46    private int scrollCount;
47    private int interval;
48    private int topFixedCount;
49    private int bottomFixedCount;
50    private int firstIndex = 0;
51    private int keepVisibleIndex = -1;
52  
53    /***
54     * Registers a menu to be scrolled with the default number of items to
55     * display at a time and the default scrolling interval.
56     * 
57     * @param menu the menu
58     * @return the MenuScroller
59     */
60    public static MenuScroller setScrollerFor(JMenu menu) {
61      return new MenuScroller(menu);
62    }
63  
64    /***
65     * Registers a popup menu to be scrolled with the default number of items to
66     * display at a time and the default scrolling interval.
67     * 
68     * @param menu the popup menu
69     * @return the MenuScroller
70     */
71    public static MenuScroller setScrollerFor(JPopupMenu menu) {
72      return new MenuScroller(menu);
73    }
74  
75    /***
76     * Registers a menu to be scrolled with the default number of items to
77     * display at a time and the specified scrolling interval.
78     * 
79     * @param menu the menu
80     * @param scrollCount the number of items to display at a time
81     * @return the MenuScroller
82     * @throws IllegalArgumentException if scrollCount is 0 or negative
83     */
84    public static MenuScroller setScrollerFor(JMenu menu, int scrollCount) {
85      return new MenuScroller(menu, scrollCount);
86    }
87  
88    /***
89     * Registers a popup menu to be scrolled with the default number of items to
90     * display at a time and the specified scrolling interval.
91     * 
92     * @param menu the popup menu
93     * @param scrollCount the number of items to display at a time
94     * @return the MenuScroller
95     * @throws IllegalArgumentException if scrollCount is 0 or negative
96     */
97    public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount) {
98      return new MenuScroller(menu, scrollCount);
99    }
100 
101   /***
102    * Registers a menu to be scrolled, with the specified number of items to
103    * display at a time and the specified scrolling interval.
104    * 
105    * @param menu the menu
106    * @param scrollCount the number of items to be displayed at a time
107    * @param interval the scroll interval, in milliseconds
108    * @return the MenuScroller
109    * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
110    */
111   public static MenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval) {
112     return new MenuScroller(menu, scrollCount, interval);
113   }
114 
115   /***
116    * Registers a popup menu to be scrolled, with the specified number of items to
117    * display at a time and the specified scrolling interval.
118    * 
119    * @param menu the popup menu
120    * @param scrollCount the number of items to be displayed at a time
121    * @param interval the scroll interval, in milliseconds
122    * @return the MenuScroller
123    * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
124    */
125   public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount, int interval) {
126     return new MenuScroller(menu, scrollCount, interval);
127   }
128 
129   /***
130    * Registers a menu to be scrolled, with the specified number of items
131    * to display in the scrolling region, the specified scrolling interval,
132    * and the specified numbers of items fixed at the top and bottom of the
133    * menu.
134    * 
135    * @param menu the menu
136    * @param scrollCount the number of items to display in the scrolling portion
137    * @param interval the scroll interval, in milliseconds
138    * @param topFixedCount the number of items to fix at the top.  May be 0.
139    * @param bottomFixedCount the number of items to fix at the bottom. May be 0
140    * @throws IllegalArgumentException if scrollCount or interval is 0 or
141    * negative or if topFixedCount or bottomFixedCount is negative
142    * @return the MenuScroller
143    */
144   public static MenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval,
145           int topFixedCount, int bottomFixedCount) {
146     return new MenuScroller(menu, scrollCount, interval,
147             topFixedCount, bottomFixedCount);
148   }
149 
150   /***
151    * Registers a popup menu to be scrolled, with the specified number of items
152    * to display in the scrolling region, the specified scrolling interval,
153    * and the specified numbers of items fixed at the top and bottom of the
154    * popup menu.
155    * 
156    * @param menu the popup menu
157    * @param scrollCount the number of items to display in the scrolling portion
158    * @param interval the scroll interval, in milliseconds
159    * @param topFixedCount the number of items to fix at the top.  May be 0
160    * @param bottomFixedCount the number of items to fix at the bottom.  May be 0
161    * @throws IllegalArgumentException if scrollCount or interval is 0 or
162    * negative or if topFixedCount or bottomFixedCount is negative
163    * @return the MenuScroller
164    */
165   public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount, int interval,
166           int topFixedCount, int bottomFixedCount) {
167     return new MenuScroller(menu, scrollCount, interval,
168             topFixedCount, bottomFixedCount);
169   }
170 
171   /***
172    * Constructs a <code>MenuScroller</code> that scrolls a menu with the
173    * default number of items to display at a time, and default scrolling
174    * interval.
175    * 
176    * @param menu the menu
177    */
178   public MenuScroller(JMenu menu) {
179     this(menu, 15);
180   }
181 
182   /***
183    * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
184    * default number of items to display at a time, and default scrolling
185    * interval.
186    * 
187    * @param menu the popup menu
188    */
189   public MenuScroller(JPopupMenu menu) {
190     this(menu, 15);
191   }
192 
193   /***
194    * Constructs a <code>MenuScroller</code> that scrolls a menu with the
195    * specified number of items to display at a time, and default scrolling
196    * interval.
197    * 
198    * @param menu the menu
199    * @param scrollCount the number of items to display at a time
200    * @throws IllegalArgumentException if scrollCount is 0 or negative
201    */
202   public MenuScroller(JMenu menu, int scrollCount) {
203     this(menu, scrollCount, 150);
204   }
205 
206   /***
207    * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
208    * specified number of items to display at a time, and default scrolling
209    * interval.
210    * 
211    * @param menu the popup menu
212    * @param scrollCount the number of items to display at a time
213    * @throws IllegalArgumentException if scrollCount is 0 or negative
214    */
215   public MenuScroller(JPopupMenu menu, int scrollCount) {
216     this(menu, scrollCount, 150);
217   }
218 
219   /***
220    * Constructs a <code>MenuScroller</code> that scrolls a menu with the
221    * specified number of items to display at a time, and specified scrolling
222    * interval.
223    * 
224    * @param menu the menu
225    * @param scrollCount the number of items to display at a time
226    * @param interval the scroll interval, in milliseconds
227    * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
228    */
229   public MenuScroller(JMenu menu, int scrollCount, int interval) {
230     this(menu, scrollCount, interval, 0, 0);
231   }
232 
233   /***
234    * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
235    * specified number of items to display at a time, and specified scrolling
236    * interval.
237    * 
238    * @param menu the popup menu
239    * @param scrollCount the number of items to display at a time
240    * @param interval the scroll interval, in milliseconds
241    * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
242    */
243   public MenuScroller(JPopupMenu menu, int scrollCount, int interval) {
244     this(menu, scrollCount, interval, 0, 0);
245   }
246 
247   /***
248    * Constructs a <code>MenuScroller</code> that scrolls a menu with the
249    * specified number of items to display in the scrolling region, the
250    * specified scrolling interval, and the specified numbers of items fixed at
251    * the top and bottom of the menu.
252    * 
253    * @param menu the menu
254    * @param scrollCount the number of items to display in the scrolling portion
255    * @param interval the scroll interval, in milliseconds
256    * @param topFixedCount the number of items to fix at the top.  May be 0
257    * @param bottomFixedCount the number of items to fix at the bottom.  May be 0
258    * @throws IllegalArgumentException if scrollCount or interval is 0 or
259    * negative or if topFixedCount or bottomFixedCount is negative
260    */
261   public MenuScroller(JMenu menu, int scrollCount, int interval,
262           int topFixedCount, int bottomFixedCount) {
263     this(menu.getPopupMenu(), scrollCount, interval, topFixedCount, bottomFixedCount);
264   }
265 
266   /***
267    * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
268    * specified number of items to display in the scrolling region, the
269    * specified scrolling interval, and the specified numbers of items fixed at
270    * the top and bottom of the popup menu.
271    * 
272    * @param menu the popup menu
273    * @param scrollCount the number of items to display in the scrolling portion
274    * @param interval the scroll interval, in milliseconds
275    * @param topFixedCount the number of items to fix at the top.  May be 0
276    * @param bottomFixedCount the number of items to fix at the bottom.  May be 0
277    * @throws IllegalArgumentException if scrollCount or interval is 0 or
278    * negative or if topFixedCount or bottomFixedCount is negative
279    */
280   public MenuScroller(JPopupMenu menu, int scrollCount, int interval,
281           int topFixedCount, int bottomFixedCount) {
282     if (scrollCount <= 0 || interval <= 0) {
283       throw new IllegalArgumentException("scrollCount and interval must be greater than 0");
284     }
285     if (topFixedCount < 0 || bottomFixedCount < 0) {
286       throw new IllegalArgumentException("topFixedCount and bottomFixedCount cannot be negative");
287     }
288 
289     upItem = new MenuScrollItem(MenuIcon.UP, -1);
290     downItem = new MenuScrollItem(MenuIcon.DOWN, +1);
291     setScrollCount(scrollCount);
292     setInterval(interval);
293     setTopFixedCount(topFixedCount);
294     setBottomFixedCount(bottomFixedCount);
295 
296     this.menu = menu;
297     menu.addPopupMenuListener(menuListener);
298   }
299 
300   /***
301    * Returns the scroll interval in milliseconds
302    * 
303    * @return the scroll interval in milliseconds
304    */
305   public int getInterval() {
306     return interval;
307   }
308 
309   /***
310    * Sets the scroll interval in milliseconds
311    * 
312    * @param interval the scroll interval in milliseconds
313    * @throws IllegalArgumentException if interval is 0 or negative
314    */
315   public void setInterval(int interval) {
316     if (interval <= 0) {
317       throw new IllegalArgumentException("interval must be greater than 0");
318     }
319     upItem.setInterval(interval);
320     downItem.setInterval(interval);
321     this.interval = interval;
322   }
323 
324   /***
325    * Returns the number of items in the scrolling portion of the menu.
326    *
327    * @return the number of items to display at a time
328    */
329   public int getscrollCount() {
330     return scrollCount;
331   }
332 
333   /***
334    * Sets the number of items in the scrolling portion of the menu.
335    * 
336    * @param scrollCount the number of items to display at a time
337    * @throws IllegalArgumentException if scrollCount is 0 or negative
338    */
339   public void setScrollCount(int scrollCount) {
340     if (scrollCount <= 0) {
341       throw new IllegalArgumentException("scrollCount must be greater than 0");
342     }
343     this.scrollCount = scrollCount;
344     MenuSelectionManager.defaultManager().clearSelectedPath();
345   }
346 
347   /***
348    * Returns the number of items fixed at the top of the menu or popup menu.
349    * 
350    * @return the number of items
351    */
352   public int getTopFixedCount() {
353     return topFixedCount;
354   }
355 
356   /***
357    * Sets the number of items to fix at the top of the menu or popup menu.
358    * 
359    * @param topFixedCount the number of items
360    */
361   public void setTopFixedCount(int topFixedCount) {
362     if (firstIndex <= topFixedCount) {
363       firstIndex = topFixedCount;
364     } else {
365       firstIndex += (topFixedCount - this.topFixedCount);
366     }
367     this.topFixedCount = topFixedCount;
368   }
369 
370   /***
371    * Returns the number of items fixed at the bottom of the menu or popup menu.
372    * 
373    * @return the number of items
374    */
375   public int getBottomFixedCount() {
376     return bottomFixedCount;
377   }
378 
379   /***
380    * Sets the number of items to fix at the bottom of the menu or popup menu.
381    * 
382    * @param bottomFixedCount the number of items
383    */
384   public void setBottomFixedCount(int bottomFixedCount) {
385     this.bottomFixedCount = bottomFixedCount;
386   }
387 
388   /***
389    * Scrolls the specified item into view each time the menu is opened.  Call this method with
390    * <code>null</code> to restore the default behavior, which is to show the menu as it last
391    * appeared.
392    *
393    * @param item the item to keep visible
394    * @see #keepVisible(int)
395    */
396   public void keepVisible(JMenuItem item) {
397     if (item == null) {
398       keepVisibleIndex = -1;
399     } else {
400       int index = menu.getComponentIndex(item);
401       keepVisibleIndex = index;
402     }
403   }
404 
405   /***
406    * Scrolls the item at the specified index into view each time the menu is opened.  Call this
407    * method with <code>-1</code> to restore the default behavior, which is to show the menu as
408    * it last appeared.
409    *
410    * @param index the index of the item to keep visible
411    * @see #keepVisible(javax.swing.JMenuItem)
412    */
413   public void keepVisible(int index) {
414     keepVisibleIndex = index;
415   }
416 
417   /***
418    * Removes this MenuScroller from the associated menu and restores the
419    * default behavior of the menu.
420    */
421   public void dispose() {
422     if (menu != null) {
423       menu.removePopupMenuListener(menuListener);
424       menu = null;
425     }
426   }
427 
428   /***
429    * Ensures that the <code>dispose</code> method of this MenuScroller is
430    * called when there are no more refrences to it.
431    * 
432    * @exception  Throwable if an error occurs.
433    * @see MenuScroller#dispose()
434    */
435   @Override
436   public void finalize() throws Throwable {
437     dispose();
438   }
439 
440   private void refreshMenu() {
441     if (menuItems != null && menuItems.length > 0) {
442       firstIndex = Math.max(topFixedCount, firstIndex);
443       firstIndex = Math.min(menuItems.length - bottomFixedCount - scrollCount, firstIndex);
444 
445       upItem.setEnabled(firstIndex > topFixedCount);
446       downItem.setEnabled(firstIndex + scrollCount < menuItems.length - bottomFixedCount);
447 
448       menu.removeAll();
449       for (int i = 0; i < topFixedCount; i++) {
450         menu.add(menuItems[i]);
451       }
452       if (topFixedCount > 0) {
453         menu.add(new JSeparator());
454       }
455 
456       menu.add(upItem);
457       for (int i = firstIndex; i < scrollCount + firstIndex; i++) {
458 	  if( i == scrollCount + firstIndex -1 && menuItems[i] instanceof JPopupMenu.Separator )
459 	      continue; // Skip separator if it is the last component in the scrolling list
460 	  menu.add(menuItems[i]);
461       }
462       menu.add(downItem);
463 
464       if (bottomFixedCount > 0) {
465         menu.add(new JSeparator());
466       }
467       for (int i = menuItems.length - bottomFixedCount; i < menuItems.length; i++) {
468 	    menu.add(menuItems[i]);
469       }
470 
471       JComponent parent = (JComponent) upItem.getParent();
472       parent.revalidate();
473       parent.repaint();
474     }
475   }
476 
477   private class MenuScrollListener implements PopupMenuListener {
478 
479     @Override
480     public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
481       setMenuItems();
482     }
483 
484     @Override
485     public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
486       restoreMenuItems();
487     }
488 
489     @Override
490     public void popupMenuCanceled(PopupMenuEvent e) {
491       restoreMenuItems();
492     }
493 
494     private void setMenuItems() {
495       menuItems = menu.getComponents();
496       
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 }