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

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

simplify MenuScroller

  • Property svn:eol-style set to native
File size: 15.8 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 /**
307 * Ensures that the <code>dispose</code> method of this MenuScroller is
308 * called when there are no more refrences to it.
309 *
310 * @throws Throwable if an error occurs.
311 * @see MenuScroller#dispose()
312 */
313 @Override
314 protected void finalize() throws Throwable {
315 dispose();
316 super.finalize();
317 }
318
319 private void refreshMenu() {
320 if (menuItems != null && menuItems.length > 0) {
321
322 int allItemsHeight = 0;
323 for (Component item : menuItems) {
324 allItemsHeight += item.getPreferredSize().height;
325 }
326
327 int allowedHeight = WindowGeometry.getMaxDimensionOnScreen(menu).height - ((JFrame) Main.parent).getInsets().top;
328
329 boolean mustSCroll = allItemsHeight > allowedHeight;
330
331 if (mustSCroll) {
332 firstIndex = Math.max(topFixedCount, firstIndex);
333 int scrollCount = computeScrollCount(firstIndex);
334 firstIndex = Math.min(menuItems.length - scrollCount, firstIndex);
335
336 upItem.setEnabled(firstIndex > topFixedCount);
337 downItem.setEnabled(firstIndex + scrollCount < menuItems.length);
338
339 menu.removeAll();
340 for (int i = 0; i < topFixedCount; i++) {
341 menu.add(menuItems[i]);
342 }
343 if (topFixedCount > 0) {
344 menu.addSeparator();
345 }
346
347 menu.add(upItem);
348 for (int i = firstIndex; i < scrollCount + firstIndex; i++) {
349 menu.add(menuItems[i]);
350 }
351 menu.add(downItem);
352
353 int preferredWidth = 0;
354 for (Component item : menuItems) {
355 preferredWidth = Math.max(preferredWidth, item.getPreferredSize().width);
356 }
357 menu.setPreferredSize(new Dimension(preferredWidth, menu.getPreferredSize().height));
358
359 } else if (!Arrays.equals(menu.getComponents(), menuItems)) {
360 // Scroll is not needed but menu is not up to date
361 menu.removeAll();
362 for (Component item : menuItems) {
363 menu.add(item);
364 }
365 }
366
367 menu.revalidate();
368 menu.repaint();
369 }
370 }
371
372 private class MenuScrollListener implements PopupMenuListener {
373
374 @Override
375 public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
376 setMenuItems();
377 }
378
379 @Override
380 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
381 restoreMenuItems();
382 }
383
384 @Override
385 public void popupMenuCanceled(PopupMenuEvent e) {
386 restoreMenuItems();
387 }
388
389 private void setMenuItems() {
390 menuItems = menu.getComponents();
391 refreshMenu();
392 }
393
394 private void restoreMenuItems() {
395 menu.removeAll();
396 for (Component component : menuItems) {
397 menu.add(component);
398 }
399 }
400 }
401
402 private class MenuScrollTimer extends Timer {
403
404 MenuScrollTimer(final int increment, int interval) {
405 super(interval, new ActionListener() {
406
407 @Override
408 public void actionPerformed(ActionEvent e) {
409 firstIndex += increment;
410 refreshMenu();
411 }
412 });
413 }
414 }
415
416 private class MenuScrollItem extends JMenuItem
417 implements ChangeListener {
418
419 private final MenuScrollTimer timer;
420
421 MenuScrollItem(MenuIcon icon, int increment, int interval) {
422 setIcon(icon);
423 setDisabledIcon(icon);
424 timer = new MenuScrollTimer(increment, interval);
425 addChangeListener(this);
426 }
427
428 @Override
429 public void stateChanged(ChangeEvent e) {
430 if (isArmed() && !timer.isRunning()) {
431 timer.start();
432 }
433 if (!isArmed() && timer.isRunning()) {
434 timer.stop();
435 }
436 }
437 }
438
439 private enum MenuIcon implements Icon {
440
441 UP(9, 1, 9),
442 DOWN(1, 9, 1);
443 private static final int[] XPOINTS = {1, 5, 9};
444 private final int[] yPoints;
445
446 MenuIcon(int... yPoints) {
447 this.yPoints = yPoints;
448 }
449
450 @Override
451 public void paintIcon(Component c, Graphics g, int x, int y) {
452 Dimension size = c.getSize();
453 Graphics g2 = g.create(size.width / 2 - 5, size.height / 2 - 5, 10, 10);
454 g2.setColor(Color.GRAY);
455 g2.drawPolygon(XPOINTS, yPoints, 3);
456 if (c.isEnabled()) {
457 g2.setColor(Color.BLACK);
458 g2.fillPolygon(XPOINTS, yPoints, 3);
459 }
460 g2.dispose();
461 }
462
463 @Override
464 public int getIconWidth() {
465 return 0;
466 }
467
468 @Override
469 public int getIconHeight() {
470 return ARROW_ICON_HEIGHT;
471 }
472 }
473
474 private class MouseScrollListener implements MouseWheelListener {
475 @Override
476 public void mouseWheelMoved(MouseWheelEvent mwe) {
477 firstIndex += mwe.getWheelRotation();
478 refreshMenu();
479 if (Main.isDebugEnabled()) {
480 Main.debug(getClass().getName()+" consuming event "+mwe);
481 }
482 mwe.consume();
483 }
484 }
485}
Note: See TracBrowser for help on using the repository browser.