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

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

fix #10207 - fix menu scroller for multi monitors systems

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