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

Last change on this file since 13425 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
RevLine 
[4593]1/**
[6246]2 * MenuScroller.java 1.5.0 04/02/12
[6920]3 * License: use / modify without restrictions (see https://tips4java.wordpress.com/about/)
[7463]4 * Heavily modified for JOSM needs => drop unused features and replace static scrollcount approach by dynamic behaviour
[4593]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;
[5664]14import java.awt.event.MouseWheelEvent;
15import java.awt.event.MouseWheelListener;
[7463]16import java.util.Arrays;
[5664]17
[4593]18import javax.swing.Icon;
[7463]19import javax.swing.JFrame;
[4593]20import javax.swing.JMenu;
21import javax.swing.JMenuItem;
22import javax.swing.JPopupMenu;
[7346]23import javax.swing.JSeparator;
[4593]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
[7291]30import org.openstreetmap.josm.Main;
[12678]31import org.openstreetmap.josm.gui.util.WindowGeometry;
[12620]32import org.openstreetmap.josm.tools.Logging;
[7291]33
[4593]34/**
35 * A class that provides scrolling capabilities to a long menu dropdown or
[7452]36 * popup menu. A number of items can optionally be frozen at the top of the menu.
[7463]37 * <p>
38 * <b>Implementation note:</B> The default scrolling interval is 150 milliseconds.
39 * <p>
[6920]40 * @author Darryl, https://tips4java.wordpress.com/2009/02/01/menu-scroller/
[7463]41 * @since 4593
[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();
[5664]50 private final MouseWheelListener mouseWheelListener = new MouseScrollListener();
[4593]51 private int topFixedCount;
[8840]52 private int firstIndex;
[4593]53
[7291]54 private static final int ARROW_ICON_HEIGHT = 10;
55
[7463]56 private int computeScrollCount(int startIndex) {
[7291]57 int result = 15;
[7463]58 if (menu != null) {
[7291]59 // Compute max height of current screen
[8510]60 int maxHeight = WindowGeometry.getMaxDimensionOnScreen(menu).height - ((JFrame) Main.parent).getInsets().top;
[7463]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;
[7291]68 }
69
[7463]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;
[7291]75
[7463]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;
[7291]81 }
[7463]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
[8510]89 for (int i = startIndex-1; i >= 0 && height <= maxHeight; i--, result++) {
[7463]90 height += menuItems[i].getPreferredSize().height;
91 }
92 if (height > maxHeight) {
93 result--;
94 }
95 }
[7291]96 }
97 return result;
98 }
99
100 /**
[7463]101 * Registers a menu to be scrolled with the default scrolling interval.
[4593]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 /**
[7463]111 * Registers a popup menu to be scrolled with the default scrolling interval.
[4593]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 /**
[7463]121 * Registers a menu to be scrolled, with the specified scrolling interval.
[4593]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
[7463]127 * @since 7463
[4593]128 */
[7463]129 public static MenuScroller setScrollerFor(JMenu menu, int interval) {
130 return new MenuScroller(menu, interval);
[4593]131 }
132
133 /**
[7463]134 * Registers a popup menu to be scrolled, with the specified scrolling interval.
[4593]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
[7463]140 * @since 7463
[4593]141 */
[7463]142 public static MenuScroller setScrollerFor(JPopupMenu menu, int interval) {
143 return new MenuScroller(menu, interval);
[4593]144 }
145
146 /**
[7463]147 * Registers a menu to be scrolled, with the specified scrolling interval,
[7452]148 * and the specified numbers of items fixed at the top of the menu.
[4593]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.
[8459]153 * @return the MenuScroller
[4593]154 * @throws IllegalArgumentException if scrollCount or interval is 0 or
[7452]155 * negative or if topFixedCount is negative
[7463]156 * @since 7463
[4593]157 */
[7463]158 public static MenuScroller setScrollerFor(JMenu menu, int interval, int topFixedCount) {
159 return new MenuScroller(menu, interval, topFixedCount);
[4593]160 }
161
162 /**
[7463]163 * Registers a popup menu to be scrolled, with the specified scrolling interval,
[7452]164 * and the specified numbers of items fixed at the top of the popup menu.
[4593]165 *
166 * @param menu the popup menu
167 * @param interval the scroll interval, in milliseconds
[7463]168 * @param topFixedCount the number of items to fix at the top. May be 0
[8459]169 * @return the MenuScroller
[4593]170 * @throws IllegalArgumentException if scrollCount or interval is 0 or
[7452]171 * negative or if topFixedCount is negative
[7463]172 * @since 7463
[4593]173 */
[7463]174 public static MenuScroller setScrollerFor(JPopupMenu menu, int interval, int topFixedCount) {
175 return new MenuScroller(menu, interval, topFixedCount);
[4593]176 }
177
178 /**
179 * Constructs a <code>MenuScroller</code> that scrolls a menu with the
[7463]180 * default scrolling interval.
[4593]181 *
182 * @param menu the menu
[7463]183 * @throws IllegalArgumentException if scrollCount is 0 or negative
[4593]184 */
185 public MenuScroller(JMenu menu) {
[7463]186 this(menu, 150);
[4593]187 }
188
189 /**
190 * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
[7463]191 * default scrolling interval.
[4593]192 *
193 * @param menu the popup menu
[7463]194 * @throws IllegalArgumentException if scrollCount is 0 or negative
[4593]195 */
196 public MenuScroller(JPopupMenu menu) {
[7463]197 this(menu, 150);
[4593]198 }
199
200 /**
201 * Constructs a <code>MenuScroller</code> that scrolls a menu with the
[7463]202 * specified scrolling interval.
[4593]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
[7463]207 * @since 7463
[4593]208 */
[7463]209 public MenuScroller(JMenu menu, int interval) {
210 this(menu, interval, 0);
[4593]211 }
212
213 /**
214 * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
[7463]215 * specified scrolling interval.
[4593]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
[7463]220 * @since 7463
[4593]221 */
[7463]222 public MenuScroller(JPopupMenu menu, int interval) {
223 this(menu, interval, 0);
[4593]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
[7452]229 * the top of the menu.
[4593]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
[7452]235 * negative or if topFixedCount is negative
[7463]236 * @since 7463
[4593]237 */
[7463]238 public MenuScroller(JMenu menu, int interval, int topFixedCount) {
239 this(menu.getPopupMenu(), interval, topFixedCount);
[4593]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
[7452]245 * the top of the popup menu.
[4593]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
[7452]251 * negative or if topFixedCount is negative
[7463]252 * @since 7463
[4593]253 */
[7463]254 public MenuScroller(JPopupMenu menu, int interval, int topFixedCount) {
255 if (interval <= 0) {
256 throw new IllegalArgumentException("interval must be greater than 0");
[4593]257 }
[7452]258 if (topFixedCount < 0) {
259 throw new IllegalArgumentException("topFixedCount cannot be negative");
[4593]260 }
261
[11506]262 upItem = new MenuScrollItem(MenuIcon.UP, -1, interval);
263 downItem = new MenuScrollItem(MenuIcon.DOWN, +1, interval);
[4593]264 setTopFixedCount(topFixedCount);
265
266 this.menu = menu;
267 menu.addPopupMenuListener(menuListener);
[5664]268 menu.addMouseWheelListener(mouseWheelListener);
[4593]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);
[5664]301 menu.removeMouseWheelListener(mouseWheelListener);
302 menu.setPreferredSize(null);
[4593]303 menu = null;
304 }
305 }
306
307 private void refreshMenu() {
308 if (menuItems != null && menuItems.length > 0) {
[7346]309
[7463]310 int allItemsHeight = 0;
311 for (Component item : menuItems) {
312 allItemsHeight += item.getPreferredSize().height;
313 }
[7346]314
[8510]315 int allowedHeight = WindowGeometry.getMaxDimensionOnScreen(menu).height - ((JFrame) Main.parent).getInsets().top;
[4593]316
[7463]317 boolean mustSCroll = allItemsHeight > allowedHeight;
[4593]318
[7463]319 if (mustSCroll) {
320 firstIndex = Math.max(topFixedCount, firstIndex);
321 int scrollCount = computeScrollCount(firstIndex);
322 firstIndex = Math.min(menuItems.length - scrollCount, firstIndex);
[4593]323
[7463]324 upItem.setEnabled(firstIndex > topFixedCount);
325 downItem.setEnabled(firstIndex + scrollCount < menuItems.length);
[4593]326
[7463]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 }
[4593]353 }
354
[7463]355 menu.revalidate();
356 menu.repaint();
[4593]357 }
358 }
[6070]359
[4593]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();
[7463]379 refreshMenu();
[4593]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
[8836]392 MenuScrollTimer(final int increment, int interval) {
[4593]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
[9078]407 private final MenuScrollTimer timer;
[4593]408
[11506]409 MenuScrollItem(MenuIcon icon, int increment, int interval) {
[4593]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
[8836]427 private enum MenuIcon implements Icon {
[4593]428
429 UP(9, 1, 9),
430 DOWN(1, 9, 1);
[8285]431 private static final int[] XPOINTS = {1, 5, 9};
432 private final int[] yPoints;
[4593]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);
[6987]443 g2.drawPolygon(XPOINTS, yPoints, 3);
[4593]444 if (c.isEnabled()) {
445 g2.setColor(Color.BLACK);
[6987]446 g2.fillPolygon(XPOINTS, yPoints, 3);
[4593]447 }
448 g2.dispose();
449 }
450
451 @Override
452 public int getIconWidth() {
453 return 0;
454 }
455
456 @Override
457 public int getIconHeight() {
[7291]458 return ARROW_ICON_HEIGHT;
[4593]459 }
460 }
[6070]461
[5664]462 private class MouseScrollListener implements MouseWheelListener {
[6084]463 @Override
[5664]464 public void mouseWheelMoved(MouseWheelEvent mwe) {
[7463]465 firstIndex += mwe.getWheelRotation();
466 refreshMenu();
[12620]467 if (Logging.isDebugEnabled()) {
468 Logging.debug("{0} consuming event {1}", getClass().getName(), mwe);
[8441]469 }
[7463]470 mwe.consume();
[5664]471 }
472 }
[4593]473}
Note: See TracBrowser for help on using the repository browser.