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

Last change on this file since 7448 was 7346, checked in by Don-vip, 10 years ago

fix #10207 - UI tuning in menu scroller: ignore JSeparator in scroll count

File size: 22.9 KB
Line 
1/**
2 * MenuScroller.java 1.5.0 04/02/12
3 * License: use / modify without restrictions (see https://tips4java.wordpress.com/about/)
4 */
5package org.openstreetmap.josm.gui;
6
7import java.awt.Color;
8import java.awt.Component;
9import java.awt.Dimension;
10import java.awt.Graphics;
11import java.awt.GraphicsConfiguration;
12import java.awt.Insets;
13import java.awt.event.ActionEvent;
14import java.awt.event.ActionListener;
15import java.awt.event.MouseWheelEvent;
16import java.awt.event.MouseWheelListener;
17
18import javax.swing.Icon;
19import javax.swing.JComponent;
20import javax.swing.JMenu;
21import javax.swing.JMenuItem;
22import javax.swing.JPopupMenu;
23import javax.swing.JSeparator;
24import javax.swing.MenuSelectionManager;
25import javax.swing.Timer;
26import javax.swing.event.ChangeEvent;
27import javax.swing.event.ChangeListener;
28import javax.swing.event.PopupMenuEvent;
29import javax.swing.event.PopupMenuListener;
30
31import org.openstreetmap.josm.Main;
32
33/**
34 * A class that provides scrolling capabilities to a long menu dropdown or
35 * popup menu. A number of items can optionally be frozen at the top and/or
36 * bottom of the menu.
37 * <P>
38 * <B>Implementation note:</B> The default number of items to display
39 * at a time is 15, and the default scrolling interval is 150 milliseconds.
40 * <P>
41 * @author Darryl, https://tips4java.wordpress.com/2009/02/01/menu-scroller/
42 */
43public class MenuScroller {
44
45 private JPopupMenu menu;
46 private Component[] menuItems;
47 private MenuScrollItem upItem;
48 private MenuScrollItem downItem;
49 private final MenuScrollListener menuListener = new MenuScrollListener();
50 private final MouseWheelListener mouseWheelListener = new MouseScrollListener();
51 private int scrollCount;
52 private int interval;
53 private int topFixedCount;
54 private int bottomFixedCount;
55 private int firstIndex = 0;
56 private int keepVisibleIndex = -1;
57
58 private static final int ARROW_ICON_HEIGHT = 10;
59
60 /**
61 * Computes the number of items to display at once for the given component and a given item height.
62 * @param comp The menu
63 * @param itemHeight Average item height
64 * @return the number of items to display at once
65 * @since 7291
66 */
67 public static int computeScrollCount(JComponent comp, int itemHeight) {
68 int result = 15;
69 if (comp != null && itemHeight > 0) {
70 // Compute max height of current screen
71 int maxHeight = 0;
72 GraphicsConfiguration gc = comp.getGraphicsConfiguration();
73 if (gc == null && Main.parent != null) {
74 gc = Main.parent.getGraphicsConfiguration();
75 }
76 if (gc != null) {
77 // Max displayable height (max screen height - vertical insets)
78 Insets insets = comp.getToolkit().getScreenInsets(gc);
79 maxHeight = gc.getBounds().height - insets.top - insets.bottom;
80 }
81
82 // Remove height of our two arrow icons + 2 pixels each for borders (arbitrary value)
83 maxHeight -= 2*(ARROW_ICON_HEIGHT+2);
84
85 if (maxHeight > 0) {
86 result = (maxHeight/itemHeight)-1;
87 }
88 }
89 return result;
90 }
91
92 /**
93 * Registers a menu to be scrolled with the default number of items to
94 * display at a time and the default scrolling interval.
95 *
96 * @param menu the menu
97 * @return the MenuScroller
98 */
99 public static MenuScroller setScrollerFor(JMenu menu) {
100 return new MenuScroller(menu);
101 }
102
103 /**
104 * Registers a popup menu to be scrolled with the default number of items to
105 * display at a time and the default scrolling interval.
106 *
107 * @param menu the popup menu
108 * @return the MenuScroller
109 */
110 public static MenuScroller setScrollerFor(JPopupMenu menu) {
111 return new MenuScroller(menu);
112 }
113
114 /**
115 * Registers a menu to be scrolled with the default number of items to
116 * display at a time and the specified scrolling interval.
117 *
118 * @param menu the menu
119 * @param scrollCount the number of items to display at a time
120 * @return the MenuScroller
121 * @throws IllegalArgumentException if scrollCount is 0 or negative
122 */
123 public static MenuScroller setScrollerFor(JMenu menu, int scrollCount) {
124 return new MenuScroller(menu, scrollCount);
125 }
126
127 /**
128 * Registers a popup menu to be scrolled with the default number of items to
129 * display at a time and the specified scrolling interval.
130 *
131 * @param menu the popup menu
132 * @param scrollCount the number of items to display at a time
133 * @return the MenuScroller
134 * @throws IllegalArgumentException if scrollCount is 0 or negative
135 */
136 public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount) {
137 return new MenuScroller(menu, scrollCount);
138 }
139
140 /**
141 * Registers a menu to be scrolled, with the specified number of items to
142 * display at a time and the specified scrolling interval.
143 *
144 * @param menu the menu
145 * @param scrollCount the number of items to be displayed at a time
146 * @param interval the scroll interval, in milliseconds
147 * @return the MenuScroller
148 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
149 */
150 public static MenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval) {
151 return new MenuScroller(menu, scrollCount, interval);
152 }
153
154 /**
155 * Registers a popup menu to be scrolled, with the specified number of items to
156 * display at a time and the specified scrolling interval.
157 *
158 * @param menu the popup menu
159 * @param scrollCount the number of items to be displayed at a time
160 * @param interval the scroll interval, in milliseconds
161 * @return the MenuScroller
162 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
163 */
164 public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount, int interval) {
165 return new MenuScroller(menu, scrollCount, interval);
166 }
167
168 /**
169 * Registers a menu to be scrolled, with the specified number of items
170 * to display in the scrolling region, the specified scrolling interval,
171 * and the specified numbers of items fixed at the top and bottom of the
172 * menu.
173 *
174 * @param menu the menu
175 * @param scrollCount the number of items to display in the scrolling portion
176 * @param interval the scroll interval, in milliseconds
177 * @param topFixedCount the number of items to fix at the top. May be 0.
178 * @param bottomFixedCount the number of items to fix at the bottom. May be 0
179 * @throws IllegalArgumentException if scrollCount or interval is 0 or
180 * negative or if topFixedCount or bottomFixedCount is negative
181 * @return the MenuScroller
182 */
183 public static MenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval,
184 int topFixedCount, int bottomFixedCount) {
185 return new MenuScroller(menu, scrollCount, interval,
186 topFixedCount, bottomFixedCount);
187 }
188
189 /**
190 * Registers a popup menu to be scrolled, with the specified number of items
191 * to display in the scrolling region, the specified scrolling interval,
192 * and the specified numbers of items fixed at the top and bottom of the
193 * popup menu.
194 *
195 * @param menu the popup menu
196 * @param scrollCount the number of items to display in the scrolling portion
197 * @param interval the scroll interval, in milliseconds
198 * @param topFixedCount the number of items to fix at the top. May be 0
199 * @param bottomFixedCount the number of items to fix at the bottom. May be 0
200 * @throws IllegalArgumentException if scrollCount or interval is 0 or
201 * negative or if topFixedCount or bottomFixedCount is negative
202 * @return the MenuScroller
203 */
204 public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount, int interval,
205 int topFixedCount, int bottomFixedCount) {
206 return new MenuScroller(menu, scrollCount, interval,
207 topFixedCount, bottomFixedCount);
208 }
209
210 /**
211 * Constructs a <code>MenuScroller</code> that scrolls a menu with the
212 * default number of items to display at a time, and default scrolling
213 * interval.
214 *
215 * @param menu the menu
216 */
217 public MenuScroller(JMenu menu) {
218 this(menu, computeScrollCount(menu, 30));
219 }
220
221 /**
222 * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
223 * default number of items to display at a time, and default scrolling
224 * interval.
225 *
226 * @param menu the popup menu
227 */
228 public MenuScroller(JPopupMenu menu) {
229 this(menu, computeScrollCount(menu, 30));
230 }
231
232 /**
233 * Constructs a <code>MenuScroller</code> that scrolls a menu with the
234 * specified number of items to display at a time, and default scrolling
235 * interval.
236 *
237 * @param menu the menu
238 * @param scrollCount the number of items to display at a time
239 * @throws IllegalArgumentException if scrollCount is 0 or negative
240 */
241 public MenuScroller(JMenu menu, int scrollCount) {
242 this(menu, scrollCount, 150);
243 }
244
245 /**
246 * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
247 * specified number of items to display at a time, and default scrolling
248 * interval.
249 *
250 * @param menu the popup menu
251 * @param scrollCount the number of items to display at a time
252 * @throws IllegalArgumentException if scrollCount is 0 or negative
253 */
254 public MenuScroller(JPopupMenu menu, int scrollCount) {
255 this(menu, scrollCount, 150);
256 }
257
258 /**
259 * Constructs a <code>MenuScroller</code> that scrolls a menu with the
260 * specified number of items to display at a time, and specified scrolling
261 * interval.
262 *
263 * @param menu the menu
264 * @param scrollCount the number of items to display at a time
265 * @param interval the scroll interval, in milliseconds
266 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
267 */
268 public MenuScroller(JMenu menu, int scrollCount, int interval) {
269 this(menu, scrollCount, interval, 0, 0);
270 }
271
272 /**
273 * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
274 * specified number of items to display at a time, and specified scrolling
275 * interval.
276 *
277 * @param menu the popup menu
278 * @param scrollCount the number of items to display at a time
279 * @param interval the scroll interval, in milliseconds
280 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
281 */
282 public MenuScroller(JPopupMenu menu, int scrollCount, int interval) {
283 this(menu, scrollCount, interval, 0, 0);
284 }
285
286 /**
287 * Constructs a <code>MenuScroller</code> that scrolls a menu with the
288 * specified number of items to display in the scrolling region, the
289 * specified scrolling interval, and the specified numbers of items fixed at
290 * the top and bottom of the menu.
291 *
292 * @param menu the menu
293 * @param scrollCount the number of items to display in the scrolling portion
294 * @param interval the scroll interval, in milliseconds
295 * @param topFixedCount the number of items to fix at the top. May be 0
296 * @param bottomFixedCount the number of items to fix at the bottom. May be 0
297 * @throws IllegalArgumentException if scrollCount or interval is 0 or
298 * negative or if topFixedCount or bottomFixedCount is negative
299 */
300 public MenuScroller(JMenu menu, int scrollCount, int interval,
301 int topFixedCount, int bottomFixedCount) {
302 this(menu.getPopupMenu(), scrollCount, interval, topFixedCount, bottomFixedCount);
303 }
304
305 /**
306 * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
307 * specified number of items to display in the scrolling region, the
308 * specified scrolling interval, and the specified numbers of items fixed at
309 * the top and bottom of the popup menu.
310 *
311 * @param menu the popup menu
312 * @param scrollCount the number of items to display in the scrolling portion
313 * @param interval the scroll interval, in milliseconds
314 * @param topFixedCount the number of items to fix at the top. May be 0
315 * @param bottomFixedCount the number of items to fix at the bottom. May be 0
316 * @throws IllegalArgumentException if scrollCount or interval is 0 or
317 * negative or if topFixedCount or bottomFixedCount is negative
318 */
319 public MenuScroller(JPopupMenu menu, int scrollCount, int interval,
320 int topFixedCount, int bottomFixedCount) {
321 if (scrollCount <= 0 || interval <= 0) {
322 throw new IllegalArgumentException("scrollCount and interval must be greater than 0");
323 }
324 if (topFixedCount < 0 || bottomFixedCount < 0) {
325 throw new IllegalArgumentException("topFixedCount and bottomFixedCount cannot be negative");
326 }
327
328 upItem = new MenuScrollItem(MenuIcon.UP, -1);
329 downItem = new MenuScrollItem(MenuIcon.DOWN, +1);
330 setScrollCount(scrollCount);
331 setInterval(interval);
332 setTopFixedCount(topFixedCount);
333 setBottomFixedCount(bottomFixedCount);
334
335 this.menu = menu;
336 menu.addPopupMenuListener(menuListener);
337 menu.addMouseWheelListener(mouseWheelListener);
338 }
339
340 /**
341 * Returns the scroll interval in milliseconds
342 *
343 * @return the scroll interval in milliseconds
344 */
345 public int getInterval() {
346 return interval;
347 }
348
349 /**
350 * Sets the scroll interval in milliseconds
351 *
352 * @param interval the scroll interval in milliseconds
353 * @throws IllegalArgumentException if interval is 0 or negative
354 */
355 public void setInterval(int interval) {
356 if (interval <= 0) {
357 throw new IllegalArgumentException("interval must be greater than 0");
358 }
359 upItem.setInterval(interval);
360 downItem.setInterval(interval);
361 this.interval = interval;
362 }
363
364 /**
365 * Returns the number of items in the scrolling portion of the menu.
366 *
367 * @return the number of items to display at a time
368 */
369 public int getscrollCount() {
370 return scrollCount;
371 }
372
373 /**
374 * Sets the number of items in the scrolling portion of the menu.
375 *
376 * @param scrollCount the number of items to display at a time
377 * @throws IllegalArgumentException if scrollCount is 0 or negative
378 */
379 public void setScrollCount(int scrollCount) {
380 if (scrollCount <= 0) {
381 throw new IllegalArgumentException("scrollCount must be greater than 0");
382 }
383 this.scrollCount = scrollCount;
384 MenuSelectionManager.defaultManager().clearSelectedPath();
385 }
386
387 /**
388 * Returns the number of items fixed at the top of the menu or popup menu.
389 *
390 * @return the number of items
391 */
392 public int getTopFixedCount() {
393 return topFixedCount;
394 }
395
396 /**
397 * Sets the number of items to fix at the top of the menu or popup menu.
398 *
399 * @param topFixedCount the number of items
400 */
401 public void setTopFixedCount(int topFixedCount) {
402 if (firstIndex <= topFixedCount) {
403 firstIndex = topFixedCount;
404 } else {
405 firstIndex += (topFixedCount - this.topFixedCount);
406 }
407 this.topFixedCount = topFixedCount;
408 }
409
410 /**
411 * Returns the number of items fixed at the bottom of the menu or popup menu.
412 *
413 * @return the number of items
414 */
415 public int getBottomFixedCount() {
416 return bottomFixedCount;
417 }
418
419 /**
420 * Sets the number of items to fix at the bottom of the menu or popup menu.
421 *
422 * @param bottomFixedCount the number of items
423 */
424 public void setBottomFixedCount(int bottomFixedCount) {
425 this.bottomFixedCount = bottomFixedCount;
426 }
427
428 /**
429 * Scrolls the specified item into view each time the menu is opened. Call this method with
430 * <code>null</code> to restore the default behavior, which is to show the menu as it last
431 * appeared.
432 *
433 * @param item the item to keep visible
434 * @see #keepVisible(int)
435 */
436 public void keepVisible(JMenuItem item) {
437 if (item == null) {
438 keepVisibleIndex = -1;
439 } else {
440 int index = menu.getComponentIndex(item);
441 keepVisibleIndex = index;
442 }
443 }
444
445 /**
446 * Scrolls the item at the specified index into view each time the menu is opened. Call this
447 * method with <code>-1</code> to restore the default behavior, which is to show the menu as
448 * it last appeared.
449 *
450 * @param index the index of the item to keep visible
451 * @see #keepVisible(javax.swing.JMenuItem)
452 */
453 public void keepVisible(int index) {
454 keepVisibleIndex = index;
455 }
456
457 /**
458 * Removes this MenuScroller from the associated menu and restores the
459 * default behavior of the menu.
460 */
461 public void dispose() {
462 if (menu != null) {
463 menu.removePopupMenuListener(menuListener);
464 menu.removeMouseWheelListener(mouseWheelListener);
465 menu.setPreferredSize(null);
466 menu = null;
467 }
468 }
469
470 /**
471 * Ensures that the <code>dispose</code> method of this MenuScroller is
472 * called when there are no more refrences to it.
473 *
474 * @exception Throwable if an error occurs.
475 * @see MenuScroller#dispose()
476 */
477 @Override
478 protected void finalize() throws Throwable {
479 dispose();
480 super.finalize();
481 }
482
483 private void refreshMenu() {
484 if (menuItems != null && menuItems.length > 0) {
485
486 int numOfNonSepItems = getNumberOfNonSeparatorItems(menuItems);
487
488 firstIndex = Math.max(topFixedCount, firstIndex);
489 firstIndex = Math.min(numOfNonSepItems - bottomFixedCount - scrollCount, firstIndex);
490
491 upItem.setEnabled(firstIndex > topFixedCount);
492 downItem.setEnabled(firstIndex + scrollCount < numOfNonSepItems - bottomFixedCount);
493
494 menu.removeAll();
495 for (int i = 0; i < topFixedCount; i++) {
496 menu.add(menuItems[i]);
497 }
498 if (topFixedCount > 0) {
499 menu.addSeparator();
500 }
501
502 menu.add(upItem);
503 for (int i = firstIndex; i < scrollCount + firstIndex; i++) {
504 menu.add(menuItems[i]);
505 }
506 menu.add(downItem);
507
508 if (bottomFixedCount > 0) {
509 menu.addSeparator();
510 }
511 for (int i = menuItems.length - bottomFixedCount; i < menuItems.length; i++) {
512 menu.add(menuItems[i]);
513 }
514
515 int preferredWidth = 0;
516 for (Component item : menuItems) {
517 preferredWidth = Math.max(preferredWidth, item.getPreferredSize().width);
518 }
519 menu.setPreferredSize(new Dimension(preferredWidth, menu.getPreferredSize().height));
520
521 JComponent parent = (JComponent) upItem.getParent();
522 parent.revalidate();
523 parent.repaint();
524 }
525 }
526
527 private class MenuScrollListener implements PopupMenuListener {
528
529 @Override
530 public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
531 setMenuItems();
532 }
533
534 @Override
535 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
536 restoreMenuItems();
537 }
538
539 @Override
540 public void popupMenuCanceled(PopupMenuEvent e) {
541 restoreMenuItems();
542 }
543
544 private void setMenuItems() {
545 menuItems = menu.getComponents();
546 int numOfNonSepItems = getNumberOfNonSeparatorItems(menuItems);
547 if (keepVisibleIndex >= topFixedCount
548 && keepVisibleIndex <= numOfNonSepItems - bottomFixedCount
549 && (keepVisibleIndex > firstIndex + scrollCount
550 || keepVisibleIndex < firstIndex)) {
551 firstIndex = Math.min(firstIndex, keepVisibleIndex);
552 firstIndex = Math.max(firstIndex, keepVisibleIndex - scrollCount + 1);
553 }
554 if (numOfNonSepItems > topFixedCount + scrollCount + bottomFixedCount) {
555 refreshMenu();
556 }
557 }
558
559 private void restoreMenuItems() {
560 menu.removeAll();
561 for (Component component : menuItems) {
562 menu.add(component);
563 }
564 }
565 }
566
567 private class MenuScrollTimer extends Timer {
568
569 public MenuScrollTimer(final int increment, int interval) {
570 super(interval, new ActionListener() {
571
572 @Override
573 public void actionPerformed(ActionEvent e) {
574 firstIndex += increment;
575 refreshMenu();
576 }
577 });
578 }
579 }
580
581 private class MenuScrollItem extends JMenuItem
582 implements ChangeListener {
583
584 private MenuScrollTimer timer;
585
586 public MenuScrollItem(MenuIcon icon, int increment) {
587 setIcon(icon);
588 setDisabledIcon(icon);
589 timer = new MenuScrollTimer(increment, interval);
590 addChangeListener(this);
591 }
592
593 public void setInterval(int interval) {
594 timer.setDelay(interval);
595 }
596
597 @Override
598 public void stateChanged(ChangeEvent e) {
599 if (isArmed() && !timer.isRunning()) {
600 timer.start();
601 }
602 if (!isArmed() && timer.isRunning()) {
603 timer.stop();
604 }
605 }
606 }
607
608 private static enum MenuIcon implements Icon {
609
610 UP(9, 1, 9),
611 DOWN(1, 9, 1);
612 static final int[] XPOINTS = {1, 5, 9};
613 final int[] yPoints;
614
615 MenuIcon(int... yPoints) {
616 this.yPoints = yPoints;
617 }
618
619 @Override
620 public void paintIcon(Component c, Graphics g, int x, int y) {
621 Dimension size = c.getSize();
622 Graphics g2 = g.create(size.width / 2 - 5, size.height / 2 - 5, 10, 10);
623 g2.setColor(Color.GRAY);
624 g2.drawPolygon(XPOINTS, yPoints, 3);
625 if (c.isEnabled()) {
626 g2.setColor(Color.BLACK);
627 g2.fillPolygon(XPOINTS, yPoints, 3);
628 }
629 g2.dispose();
630 }
631
632 @Override
633 public int getIconWidth() {
634 return 0;
635 }
636
637 @Override
638 public int getIconHeight() {
639 return ARROW_ICON_HEIGHT;
640 }
641 }
642
643 private class MouseScrollListener implements MouseWheelListener {
644 @Override
645 public void mouseWheelMoved(MouseWheelEvent mwe) {
646 if (getNumberOfNonSeparatorItems(menu.getComponents()) > scrollCount) {
647 firstIndex += mwe.getWheelRotation();
648 refreshMenu();
649 }
650 mwe.consume(); // (Comment 16, Huw)
651 }
652 }
653
654 private int getNumberOfNonSeparatorItems(Component[] items) {
655 int result = 0;
656 for (Component c : items) {
657 if (!(c instanceof JSeparator)) {
658 result++;
659 }
660 }
661 return result;
662 }
663}
Note: See TracBrowser for help on using the repository browser.