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

Last change on this file since 12288 was 12234, checked in by Don-vip, 7 years ago

see #11924 - see JDK-8165641: Object.finalize() is deprecated

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