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

Last change on this file since 10998 was 9078, checked in by Don-vip, 8 years ago

sonar - Immutable Field

  • Property svn:eol-style set to native
File size: 16.6 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 * Heavily modified for JOSM needs => drop unused features and replace static scrollcount approach by dynamic behaviour
5 */
6package org.openstreetmap.josm.gui;
7
8import java.awt.Color;
9import java.awt.Component;
10import java.awt.Dimension;
11import java.awt.Graphics;
12import java.awt.event.ActionEvent;
13import java.awt.event.ActionListener;
14import java.awt.event.MouseWheelEvent;
15import java.awt.event.MouseWheelListener;
16import java.util.Arrays;
17
18import javax.swing.Icon;
19import javax.swing.JFrame;
20import javax.swing.JMenu;
21import javax.swing.JMenuItem;
22import javax.swing.JPopupMenu;
23import javax.swing.JSeparator;
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;
31import org.openstreetmap.josm.tools.WindowGeometry;
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 of the menu.
36 * <p>
37 * <b>Implementation note:</B> The default scrolling interval is 150 milliseconds.
38 * <p>
39 * @author Darryl, https://tips4java.wordpress.com/2009/02/01/menu-scroller/
40 * @since 4593
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 interval;
51 private int topFixedCount;
52 private int firstIndex;
53
54 private static final int ARROW_ICON_HEIGHT = 10;
55
56 private int computeScrollCount(int startIndex) {
57 int result = 15;
58 if (menu != null) {
59 // Compute max height of current screen
60 int maxHeight = WindowGeometry.getMaxDimensionOnScreen(menu).height - ((JFrame) Main.parent).getInsets().top;
61
62 // Remove top fixed part height
63 if (topFixedCount > 0) {
64 for (int i = 0; i < topFixedCount; i++) {
65 maxHeight -= menuItems[i].getPreferredSize().height;
66 }
67 maxHeight -= new JSeparator().getPreferredSize().height;
68 }
69
70 // Remove height of our two arrow items + insets
71 maxHeight -= menu.getInsets().top;
72 maxHeight -= upItem.getPreferredSize().height;
73 maxHeight -= downItem.getPreferredSize().height;
74 maxHeight -= menu.getInsets().bottom;
75
76 // Compute scroll count
77 result = 0;
78 int height = 0;
79 for (int i = startIndex; i < menuItems.length && height <= maxHeight; i++, result++) {
80 height += menuItems[i].getPreferredSize().height;
81 }
82
83 if (height > maxHeight) {
84 // Remove extra item from count
85 result--;
86 } else {
87 // Increase scroll count to take into account upper items that will be displayed
88 // after firstIndex is updated
89 for (int i = startIndex-1; i >= 0 && height <= maxHeight; i--, result++) {
90 height += menuItems[i].getPreferredSize().height;
91 }
92 if (height > maxHeight) {
93 result--;
94 }
95 }
96 }
97 return result;
98 }
99
100 /**
101 * Registers a menu to be scrolled with the default scrolling interval.
102 *
103 * @param menu the menu
104 * @return the MenuScroller
105 */
106 public static MenuScroller setScrollerFor(JMenu menu) {
107 return new MenuScroller(menu);
108 }
109
110 /**
111 * Registers a popup menu to be scrolled with the default scrolling interval.
112 *
113 * @param menu the popup menu
114 * @return the MenuScroller
115 */
116 public static MenuScroller setScrollerFor(JPopupMenu menu) {
117 return new MenuScroller(menu);
118 }
119
120 /**
121 * Registers a menu to be scrolled, with the specified scrolling interval.
122 *
123 * @param menu the menu
124 * @param interval the scroll interval, in milliseconds
125 * @return the MenuScroller
126 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
127 * @since 7463
128 */
129 public static MenuScroller setScrollerFor(JMenu menu, int interval) {
130 return new MenuScroller(menu, interval);
131 }
132
133 /**
134 * Registers a popup menu to be scrolled, with the specified scrolling interval.
135 *
136 * @param menu the popup menu
137 * @param interval the scroll interval, in milliseconds
138 * @return the MenuScroller
139 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
140 * @since 7463
141 */
142 public static MenuScroller setScrollerFor(JPopupMenu menu, int interval) {
143 return new MenuScroller(menu, interval);
144 }
145
146 /**
147 * Registers a menu to be scrolled, with the specified scrolling interval,
148 * and the specified numbers of items fixed at the top of the menu.
149 *
150 * @param menu the menu
151 * @param interval the scroll interval, in milliseconds
152 * @param topFixedCount the number of items to fix at the top. May be 0.
153 * @return the MenuScroller
154 * @throws IllegalArgumentException if scrollCount or interval is 0 or
155 * negative or if topFixedCount is negative
156 * @since 7463
157 */
158 public static MenuScroller setScrollerFor(JMenu menu, int interval, int topFixedCount) {
159 return new MenuScroller(menu, interval, topFixedCount);
160 }
161
162 /**
163 * Registers a popup menu to be scrolled, with the specified scrolling interval,
164 * and the specified numbers of items fixed at the top of the popup menu.
165 *
166 * @param menu the popup menu
167 * @param interval the scroll interval, in milliseconds
168 * @param topFixedCount the number of items to fix at the top. May be 0
169 * @return the MenuScroller
170 * @throws IllegalArgumentException if scrollCount or interval is 0 or
171 * negative or if topFixedCount is negative
172 * @since 7463
173 */
174 public static MenuScroller setScrollerFor(JPopupMenu menu, int interval, int topFixedCount) {
175 return new MenuScroller(menu, interval, topFixedCount);
176 }
177
178 /**
179 * Constructs a <code>MenuScroller</code> that scrolls a menu with the
180 * default scrolling interval.
181 *
182 * @param menu the menu
183 * @throws IllegalArgumentException if scrollCount is 0 or negative
184 */
185 public MenuScroller(JMenu menu) {
186 this(menu, 150);
187 }
188
189 /**
190 * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
191 * default scrolling interval.
192 *
193 * @param menu the popup menu
194 * @throws IllegalArgumentException if scrollCount is 0 or negative
195 */
196 public MenuScroller(JPopupMenu menu) {
197 this(menu, 150);
198 }
199
200 /**
201 * Constructs a <code>MenuScroller</code> that scrolls a menu with the
202 * specified scrolling interval.
203 *
204 * @param menu the menu
205 * @param interval the scroll interval, in milliseconds
206 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
207 * @since 7463
208 */
209 public MenuScroller(JMenu menu, int interval) {
210 this(menu, interval, 0);
211 }
212
213 /**
214 * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
215 * specified scrolling interval.
216 *
217 * @param menu the popup menu
218 * @param interval the scroll interval, in milliseconds
219 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
220 * @since 7463
221 */
222 public MenuScroller(JPopupMenu menu, int interval) {
223 this(menu, interval, 0);
224 }
225
226 /**
227 * Constructs a <code>MenuScroller</code> that scrolls a menu with the
228 * specified scrolling interval, and the specified numbers of items fixed at
229 * the top of the menu.
230 *
231 * @param menu the menu
232 * @param interval the scroll interval, in milliseconds
233 * @param topFixedCount the number of items to fix at the top. May be 0
234 * @throws IllegalArgumentException if scrollCount or interval is 0 or
235 * negative or if topFixedCount is negative
236 * @since 7463
237 */
238 public MenuScroller(JMenu menu, int interval, int topFixedCount) {
239 this(menu.getPopupMenu(), interval, topFixedCount);
240 }
241
242 /**
243 * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
244 * specified scrolling interval, and the specified numbers of items fixed at
245 * the top of the popup menu.
246 *
247 * @param menu the popup menu
248 * @param interval the scroll interval, in milliseconds
249 * @param topFixedCount the number of items to fix at the top. May be 0
250 * @throws IllegalArgumentException if scrollCount or interval is 0 or
251 * negative or if topFixedCount is negative
252 * @since 7463
253 */
254 public MenuScroller(JPopupMenu menu, int interval, int topFixedCount) {
255 if (interval <= 0) {
256 throw new IllegalArgumentException("interval must be greater than 0");
257 }
258 if (topFixedCount < 0) {
259 throw new IllegalArgumentException("topFixedCount cannot be negative");
260 }
261
262 upItem = new MenuScrollItem(MenuIcon.UP, -1);
263 downItem = new MenuScrollItem(MenuIcon.DOWN, +1);
264 setInterval(interval);
265 setTopFixedCount(topFixedCount);
266
267 this.menu = menu;
268 menu.addPopupMenuListener(menuListener);
269 menu.addMouseWheelListener(mouseWheelListener);
270 }
271
272 /**
273 * Returns the scroll interval in milliseconds
274 *
275 * @return the scroll interval in milliseconds
276 */
277 public int getInterval() {
278 return interval;
279 }
280
281 /**
282 * Sets the scroll interval in milliseconds
283 *
284 * @param interval the scroll interval in milliseconds
285 * @throws IllegalArgumentException if interval is 0 or negative
286 */
287 public void setInterval(int interval) {
288 if (interval <= 0) {
289 throw new IllegalArgumentException("interval must be greater than 0");
290 }
291 upItem.setInterval(interval);
292 downItem.setInterval(interval);
293 this.interval = interval;
294 }
295
296 /**
297 * Returns the number of items fixed at the top of the menu or popup menu.
298 *
299 * @return the number of items
300 */
301 public int getTopFixedCount() {
302 return topFixedCount;
303 }
304
305 /**
306 * Sets the number of items to fix at the top of the menu or popup menu.
307 *
308 * @param topFixedCount the number of items
309 */
310 public void setTopFixedCount(int topFixedCount) {
311 if (firstIndex <= topFixedCount) {
312 firstIndex = topFixedCount;
313 } else {
314 firstIndex += (topFixedCount - this.topFixedCount);
315 }
316 this.topFixedCount = topFixedCount;
317 }
318
319 /**
320 * Removes this MenuScroller from the associated menu and restores the
321 * default behavior of the menu.
322 */
323 public void dispose() {
324 if (menu != null) {
325 menu.removePopupMenuListener(menuListener);
326 menu.removeMouseWheelListener(mouseWheelListener);
327 menu.setPreferredSize(null);
328 menu = null;
329 }
330 }
331
332 /**
333 * Ensures that the <code>dispose</code> method of this MenuScroller is
334 * called when there are no more refrences to it.
335 *
336 * @throws Throwable if an error occurs.
337 * @see MenuScroller#dispose()
338 */
339 @Override
340 protected void finalize() throws Throwable {
341 dispose();
342 super.finalize();
343 }
344
345 private void refreshMenu() {
346 if (menuItems != null && menuItems.length > 0) {
347
348 int allItemsHeight = 0;
349 for (Component item : menuItems) {
350 allItemsHeight += item.getPreferredSize().height;
351 }
352
353 int allowedHeight = WindowGeometry.getMaxDimensionOnScreen(menu).height - ((JFrame) Main.parent).getInsets().top;
354
355 boolean mustSCroll = allItemsHeight > allowedHeight;
356
357 if (mustSCroll) {
358 firstIndex = Math.max(topFixedCount, firstIndex);
359 int scrollCount = computeScrollCount(firstIndex);
360 firstIndex = Math.min(menuItems.length - scrollCount, firstIndex);
361
362 upItem.setEnabled(firstIndex > topFixedCount);
363 downItem.setEnabled(firstIndex + scrollCount < menuItems.length);
364
365 menu.removeAll();
366 for (int i = 0; i < topFixedCount; i++) {
367 menu.add(menuItems[i]);
368 }
369 if (topFixedCount > 0) {
370 menu.addSeparator();
371 }
372
373 menu.add(upItem);
374 for (int i = firstIndex; i < scrollCount + firstIndex; i++) {
375 menu.add(menuItems[i]);
376 }
377 menu.add(downItem);
378
379 int preferredWidth = 0;
380 for (Component item : menuItems) {
381 preferredWidth = Math.max(preferredWidth, item.getPreferredSize().width);
382 }
383 menu.setPreferredSize(new Dimension(preferredWidth, menu.getPreferredSize().height));
384
385 } else if (!Arrays.equals(menu.getComponents(), menuItems)) {
386 // Scroll is not needed but menu is not up to date
387 menu.removeAll();
388 for (Component item : menuItems) {
389 menu.add(item);
390 }
391 }
392
393 menu.revalidate();
394 menu.repaint();
395 }
396 }
397
398 private class MenuScrollListener implements PopupMenuListener {
399
400 @Override
401 public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
402 setMenuItems();
403 }
404
405 @Override
406 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
407 restoreMenuItems();
408 }
409
410 @Override
411 public void popupMenuCanceled(PopupMenuEvent e) {
412 restoreMenuItems();
413 }
414
415 private void setMenuItems() {
416 menuItems = menu.getComponents();
417 refreshMenu();
418 }
419
420 private void restoreMenuItems() {
421 menu.removeAll();
422 for (Component component : menuItems) {
423 menu.add(component);
424 }
425 }
426 }
427
428 private class MenuScrollTimer extends Timer {
429
430 MenuScrollTimer(final int increment, int interval) {
431 super(interval, new ActionListener() {
432
433 @Override
434 public void actionPerformed(ActionEvent e) {
435 firstIndex += increment;
436 refreshMenu();
437 }
438 });
439 }
440 }
441
442 private class MenuScrollItem extends JMenuItem
443 implements ChangeListener {
444
445 private final MenuScrollTimer timer;
446
447 MenuScrollItem(MenuIcon icon, int increment) {
448 setIcon(icon);
449 setDisabledIcon(icon);
450 timer = new MenuScrollTimer(increment, interval);
451 addChangeListener(this);
452 }
453
454 public void setInterval(int interval) {
455 timer.setDelay(interval);
456 }
457
458 @Override
459 public void stateChanged(ChangeEvent e) {
460 if (isArmed() && !timer.isRunning()) {
461 timer.start();
462 }
463 if (!isArmed() && timer.isRunning()) {
464 timer.stop();
465 }
466 }
467 }
468
469 private enum MenuIcon implements Icon {
470
471 UP(9, 1, 9),
472 DOWN(1, 9, 1);
473 private static final int[] XPOINTS = {1, 5, 9};
474 private final int[] yPoints;
475
476 MenuIcon(int... yPoints) {
477 this.yPoints = yPoints;
478 }
479
480 @Override
481 public void paintIcon(Component c, Graphics g, int x, int y) {
482 Dimension size = c.getSize();
483 Graphics g2 = g.create(size.width / 2 - 5, size.height / 2 - 5, 10, 10);
484 g2.setColor(Color.GRAY);
485 g2.drawPolygon(XPOINTS, yPoints, 3);
486 if (c.isEnabled()) {
487 g2.setColor(Color.BLACK);
488 g2.fillPolygon(XPOINTS, yPoints, 3);
489 }
490 g2.dispose();
491 }
492
493 @Override
494 public int getIconWidth() {
495 return 0;
496 }
497
498 @Override
499 public int getIconHeight() {
500 return ARROW_ICON_HEIGHT;
501 }
502 }
503
504 private class MouseScrollListener implements MouseWheelListener {
505 @Override
506 public void mouseWheelMoved(MouseWheelEvent mwe) {
507 firstIndex += mwe.getWheelRotation();
508 refreshMenu();
509 if (Main.isDebugEnabled()) {
510 Main.debug(getClass().getName()+" consuming event "+mwe);
511 }
512 mwe.consume();
513 }
514 }
515}
Note: See TracBrowser for help on using the repository browser.