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

Last change on this file since 15835 was 14173, checked in by Don-vip, 6 years ago

fix #16660 - ArrayIndexOutOfBoundsException in MenuScroller

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