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

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

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

File size: 20.2 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 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.