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

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

see #15182 - move WindowGeometry from tools to gui.util

  • 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.gui.util.WindowGeometry;
32import org.openstreetmap.josm.tools.Logging;
33
34/**
35 * A class that provides scrolling capabilities to a long menu dropdown or
36 * popup menu. A number of items can optionally be frozen at the top of the menu.
37 * <p>
38 * <b>Implementation note:</B> The default scrolling interval is 150 milliseconds.
39 * <p>
40 * @author Darryl, https://tips4java.wordpress.com/2009/02/01/menu-scroller/
41 * @since 4593
42 */
43public class MenuScroller {
44
45 private JPopupMenu menu;
46 private Component[] menuItems;
47 private MenuScrollItem upItem;
48 private MenuScrollItem downItem;
49 private final MenuScrollListener menuListener = new MenuScrollListener();
50 private final MouseWheelListener mouseWheelListener = new MouseScrollListener();
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, interval);
263 downItem = new MenuScrollItem(MenuIcon.DOWN, +1, interval);
264 setTopFixedCount(topFixedCount);
265
266 this.menu = menu;
267 menu.addPopupMenuListener(menuListener);
268 menu.addMouseWheelListener(mouseWheelListener);
269 }
270
271 /**
272 * Returns the number of items fixed at the top of the menu or popup menu.
273 *
274 * @return the number of items
275 */
276 public int getTopFixedCount() {
277 return topFixedCount;
278 }
279
280 /**
281 * Sets the number of items to fix at the top of the menu or popup menu.
282 *
283 * @param topFixedCount the number of items
284 */
285 public void setTopFixedCount(int topFixedCount) {
286 if (firstIndex <= topFixedCount) {
287 firstIndex = topFixedCount;
288 } else {
289 firstIndex += (topFixedCount - this.topFixedCount);
290 }
291 this.topFixedCount = topFixedCount;
292 }
293
294 /**
295 * Removes this MenuScroller from the associated menu and restores the
296 * default behavior of the menu.
297 */
298 public void dispose() {
299 if (menu != null) {
300 menu.removePopupMenuListener(menuListener);
301 menu.removeMouseWheelListener(mouseWheelListener);
302 menu.setPreferredSize(null);
303 menu = null;
304 }
305 }
306
307 private void refreshMenu() {
308 if (menuItems != null && menuItems.length > 0) {
309
310 int allItemsHeight = 0;
311 for (Component item : menuItems) {
312 allItemsHeight += item.getPreferredSize().height;
313 }
314
315 int allowedHeight = WindowGeometry.getMaxDimensionOnScreen(menu).height - ((JFrame) Main.parent).getInsets().top;
316
317 boolean mustSCroll = allItemsHeight > allowedHeight;
318
319 if (mustSCroll) {
320 firstIndex = Math.max(topFixedCount, firstIndex);
321 int scrollCount = computeScrollCount(firstIndex);
322 firstIndex = Math.min(menuItems.length - scrollCount, firstIndex);
323
324 upItem.setEnabled(firstIndex > topFixedCount);
325 downItem.setEnabled(firstIndex + scrollCount < menuItems.length);
326
327 menu.removeAll();
328 for (int i = 0; i < topFixedCount; i++) {
329 menu.add(menuItems[i]);
330 }
331 if (topFixedCount > 0) {
332 menu.addSeparator();
333 }
334
335 menu.add(upItem);
336 for (int i = firstIndex; i < scrollCount + firstIndex; i++) {
337 menu.add(menuItems[i]);
338 }
339 menu.add(downItem);
340
341 int preferredWidth = 0;
342 for (Component item : menuItems) {
343 preferredWidth = Math.max(preferredWidth, item.getPreferredSize().width);
344 }
345 menu.setPreferredSize(new Dimension(preferredWidth, menu.getPreferredSize().height));
346
347 } else if (!Arrays.equals(menu.getComponents(), menuItems)) {
348 // Scroll is not needed but menu is not up to date
349 menu.removeAll();
350 for (Component item : menuItems) {
351 menu.add(item);
352 }
353 }
354
355 menu.revalidate();
356 menu.repaint();
357 }
358 }
359
360 private class MenuScrollListener implements PopupMenuListener {
361
362 @Override
363 public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
364 setMenuItems();
365 }
366
367 @Override
368 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
369 restoreMenuItems();
370 }
371
372 @Override
373 public void popupMenuCanceled(PopupMenuEvent e) {
374 restoreMenuItems();
375 }
376
377 private void setMenuItems() {
378 menuItems = menu.getComponents();
379 refreshMenu();
380 }
381
382 private void restoreMenuItems() {
383 menu.removeAll();
384 for (Component component : menuItems) {
385 menu.add(component);
386 }
387 }
388 }
389
390 private class MenuScrollTimer extends Timer {
391
392 MenuScrollTimer(final int increment, int interval) {
393 super(interval, new ActionListener() {
394
395 @Override
396 public void actionPerformed(ActionEvent e) {
397 firstIndex += increment;
398 refreshMenu();
399 }
400 });
401 }
402 }
403
404 private class MenuScrollItem extends JMenuItem
405 implements ChangeListener {
406
407 private final MenuScrollTimer timer;
408
409 MenuScrollItem(MenuIcon icon, int increment, int interval) {
410 setIcon(icon);
411 setDisabledIcon(icon);
412 timer = new MenuScrollTimer(increment, interval);
413 addChangeListener(this);
414 }
415
416 @Override
417 public void stateChanged(ChangeEvent e) {
418 if (isArmed() && !timer.isRunning()) {
419 timer.start();
420 }
421 if (!isArmed() && timer.isRunning()) {
422 timer.stop();
423 }
424 }
425 }
426
427 private enum MenuIcon implements Icon {
428
429 UP(9, 1, 9),
430 DOWN(1, 9, 1);
431 private static final int[] XPOINTS = {1, 5, 9};
432 private final int[] yPoints;
433
434 MenuIcon(int... yPoints) {
435 this.yPoints = yPoints;
436 }
437
438 @Override
439 public void paintIcon(Component c, Graphics g, int x, int y) {
440 Dimension size = c.getSize();
441 Graphics g2 = g.create(size.width / 2 - 5, size.height / 2 - 5, 10, 10);
442 g2.setColor(Color.GRAY);
443 g2.drawPolygon(XPOINTS, yPoints, 3);
444 if (c.isEnabled()) {
445 g2.setColor(Color.BLACK);
446 g2.fillPolygon(XPOINTS, yPoints, 3);
447 }
448 g2.dispose();
449 }
450
451 @Override
452 public int getIconWidth() {
453 return 0;
454 }
455
456 @Override
457 public int getIconHeight() {
458 return ARROW_ICON_HEIGHT;
459 }
460 }
461
462 private class MouseScrollListener implements MouseWheelListener {
463 @Override
464 public void mouseWheelMoved(MouseWheelEvent mwe) {
465 firstIndex += mwe.getWheelRotation();
466 refreshMenu();
467 if (Logging.isDebugEnabled()) {
468 Logging.debug("{0} consuming event {1}", getClass().getName(), mwe);
469 }
470 mwe.consume();
471 }
472 }
473}
Note: See TracBrowser for help on using the repository browser.