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
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;
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 }