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

Last change on this file since 4593 was 4593, checked in by simon04, 12 years ago

see #5638 - scroll long menus (thanks to tips4java.wordpress.com)

File size: 20.4 KB
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 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}
Note: See TracBrowser for help on using the repository browser.