001/** 002 * MenuScroller.java 1.5.0 04/02/12 003 * License: use / modify without restrictions (see https://tips4java.wordpress.com/about/) 004 * Heavily modified for JOSM needs => drop unused features and replace static scrollcount approach by dynamic behaviour 005 */ 006package org.openstreetmap.josm.gui; 007 008import java.awt.Color; 009import java.awt.Component; 010import java.awt.Dimension; 011import java.awt.Graphics; 012import java.awt.event.ActionEvent; 013import java.awt.event.ActionListener; 014import java.awt.event.MouseWheelEvent; 015import java.awt.event.MouseWheelListener; 016import java.util.Arrays; 017 018import javax.swing.Icon; 019import javax.swing.JFrame; 020import javax.swing.JMenu; 021import javax.swing.JMenuItem; 022import javax.swing.JPopupMenu; 023import javax.swing.JSeparator; 024import javax.swing.Timer; 025import javax.swing.event.ChangeEvent; 026import javax.swing.event.ChangeListener; 027import javax.swing.event.PopupMenuEvent; 028import javax.swing.event.PopupMenuListener; 029 030import org.openstreetmap.josm.Main; 031import org.openstreetmap.josm.tools.WindowGeometry; 032 033/** 034 * A class that provides scrolling capabilities to a long menu dropdown or 035 * popup menu. A number of items can optionally be frozen at the top of the menu. 036 * <p> 037 * <b>Implementation note:</B> The default scrolling interval is 150 milliseconds. 038 * <p> 039 * @author Darryl, https://tips4java.wordpress.com/2009/02/01/menu-scroller/ 040 * @since 4593 041 */ 042public class MenuScroller { 043 044 private JPopupMenu menu; 045 private Component[] menuItems; 046 private MenuScrollItem upItem; 047 private MenuScrollItem downItem; 048 private final MenuScrollListener menuListener = new MenuScrollListener(); 049 private final MouseWheelListener mouseWheelListener = new MouseScrollListener(); 050 private int interval; 051 private int topFixedCount; 052 private int firstIndex = 0; 053 054 private static final int ARROW_ICON_HEIGHT = 10; 055 056 private int computeScrollCount(int startIndex) { 057 int result = 15; 058 if (menu != null) { 059 // Compute max height of current screen 060 int maxHeight = WindowGeometry.getMaxDimensionOnScreen(menu).height - ((JFrame)Main.parent).getInsets().top; 061 062 // Remove top fixed part height 063 if (topFixedCount > 0) { 064 for (int i = 0; i < topFixedCount; i++) { 065 maxHeight -= menuItems[i].getPreferredSize().height; 066 } 067 maxHeight -= new JSeparator().getPreferredSize().height; 068 } 069 070 // Remove height of our two arrow items + insets 071 maxHeight -= menu.getInsets().top; 072 maxHeight -= upItem.getPreferredSize().height; 073 maxHeight -= downItem.getPreferredSize().height; 074 maxHeight -= menu.getInsets().bottom; 075 076 // Compute scroll count 077 result = 0; 078 int height = 0; 079 for (int i = startIndex; i < menuItems.length && height <= maxHeight; i++, result++) { 080 height += menuItems[i].getPreferredSize().height; 081 } 082 083 if (height > maxHeight) { 084 // Remove extra item from count 085 result--; 086 } else { 087 // Increase scroll count to take into account upper items that will be displayed 088 // after firstIndex is updated 089 for (int i=startIndex-1; i >= 0 && height <= maxHeight; i--, result++) { 090 height += menuItems[i].getPreferredSize().height; 091 } 092 if (height > maxHeight) { 093 result--; 094 } 095 } 096 } 097 return result; 098 } 099 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 * @throws IllegalArgumentException if scrollCount or interval is 0 or 154 * negative or if topFixedCount is negative 155 * @return the MenuScroller 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 * @throws IllegalArgumentException if scrollCount or interval is 0 or 170 * negative or if topFixedCount is negative 171 * @return the MenuScroller 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); 263 downItem = new MenuScrollItem(MenuIcon.DOWN, +1); 264 setInterval(interval); 265 setTopFixedCount(topFixedCount); 266 267 this.menu = menu; 268 menu.addPopupMenuListener(menuListener); 269 menu.addMouseWheelListener(mouseWheelListener); 270 } 271 272 /** 273 * Returns the scroll interval in milliseconds 274 * 275 * @return the scroll interval in milliseconds 276 */ 277 public int getInterval() { 278 return interval; 279 } 280 281 /** 282 * Sets the scroll interval in milliseconds 283 * 284 * @param interval the scroll interval in milliseconds 285 * @throws IllegalArgumentException if interval is 0 or negative 286 */ 287 public void setInterval(int interval) { 288 if (interval <= 0) { 289 throw new IllegalArgumentException("interval must be greater than 0"); 290 } 291 upItem.setInterval(interval); 292 downItem.setInterval(interval); 293 this.interval = interval; 294 } 295 296 /** 297 * Returns the number of items fixed at the top of the menu or popup menu. 298 * 299 * @return the number of items 300 */ 301 public int getTopFixedCount() { 302 return topFixedCount; 303 } 304 305 /** 306 * Sets the number of items to fix at the top of the menu or popup menu. 307 * 308 * @param topFixedCount the number of items 309 */ 310 public void setTopFixedCount(int topFixedCount) { 311 if (firstIndex <= topFixedCount) { 312 firstIndex = topFixedCount; 313 } else { 314 firstIndex += (topFixedCount - this.topFixedCount); 315 } 316 this.topFixedCount = topFixedCount; 317 } 318 319 /** 320 * Removes this MenuScroller from the associated menu and restores the 321 * default behavior of the menu. 322 */ 323 public void dispose() { 324 if (menu != null) { 325 menu.removePopupMenuListener(menuListener); 326 menu.removeMouseWheelListener(mouseWheelListener); 327 menu.setPreferredSize(null); 328 menu = null; 329 } 330 } 331 332 /** 333 * Ensures that the <code>dispose</code> method of this MenuScroller is 334 * called when there are no more refrences to it. 335 * 336 * @exception Throwable if an error occurs. 337 * @see MenuScroller#dispose() 338 */ 339 @Override 340 protected void finalize() throws Throwable { 341 dispose(); 342 super.finalize(); 343 } 344 345 private void refreshMenu() { 346 if (menuItems != null && menuItems.length > 0) { 347 348 int allItemsHeight = 0; 349 for (Component item : menuItems) { 350 allItemsHeight += item.getPreferredSize().height; 351 } 352 353 int allowedHeight = WindowGeometry.getMaxDimensionOnScreen(menu).height - ((JFrame)Main.parent).getInsets().top; 354 355 boolean mustSCroll = allItemsHeight > allowedHeight; 356 357 if (mustSCroll) { 358 firstIndex = Math.max(topFixedCount, firstIndex); 359 int scrollCount = computeScrollCount(firstIndex); 360 firstIndex = Math.min(menuItems.length - scrollCount, firstIndex); 361 362 upItem.setEnabled(firstIndex > topFixedCount); 363 downItem.setEnabled(firstIndex + scrollCount < menuItems.length); 364 365 menu.removeAll(); 366 for (int i = 0; i < topFixedCount; i++) { 367 menu.add(menuItems[i]); 368 } 369 if (topFixedCount > 0) { 370 menu.addSeparator(); 371 } 372 373 menu.add(upItem); 374 for (int i = firstIndex; i < scrollCount + firstIndex; i++) { 375 menu.add(menuItems[i]); 376 } 377 menu.add(downItem); 378 379 int preferredWidth = 0; 380 for (Component item : menuItems) { 381 preferredWidth = Math.max(preferredWidth, item.getPreferredSize().width); 382 } 383 menu.setPreferredSize(new Dimension(preferredWidth, menu.getPreferredSize().height)); 384 385 } else if (!Arrays.equals(menu.getComponents(), menuItems)) { 386 // Scroll is not needed but menu is not up to date 387 menu.removeAll(); 388 for (Component item : menuItems) { 389 menu.add(item); 390 } 391 } 392 393 menu.revalidate(); 394 menu.repaint(); 395 } 396 } 397 398 private class MenuScrollListener implements PopupMenuListener { 399 400 @Override 401 public void popupMenuWillBecomeVisible(PopupMenuEvent e) { 402 setMenuItems(); 403 } 404 405 @Override 406 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { 407 restoreMenuItems(); 408 } 409 410 @Override 411 public void popupMenuCanceled(PopupMenuEvent e) { 412 restoreMenuItems(); 413 } 414 415 private void setMenuItems() { 416 menuItems = menu.getComponents(); 417 refreshMenu(); 418 } 419 420 private void restoreMenuItems() { 421 menu.removeAll(); 422 for (Component component : menuItems) { 423 menu.add(component); 424 } 425 } 426 } 427 428 private class MenuScrollTimer extends Timer { 429 430 public MenuScrollTimer(final int increment, int interval) { 431 super(interval, new ActionListener() { 432 433 @Override 434 public void actionPerformed(ActionEvent e) { 435 firstIndex += increment; 436 refreshMenu(); 437 } 438 }); 439 } 440 } 441 442 private class MenuScrollItem extends JMenuItem 443 implements ChangeListener { 444 445 private MenuScrollTimer timer; 446 447 public MenuScrollItem(MenuIcon icon, int increment) { 448 setIcon(icon); 449 setDisabledIcon(icon); 450 timer = new MenuScrollTimer(increment, interval); 451 addChangeListener(this); 452 } 453 454 public void setInterval(int interval) { 455 timer.setDelay(interval); 456 } 457 458 @Override 459 public void stateChanged(ChangeEvent e) { 460 if (isArmed() && !timer.isRunning()) { 461 timer.start(); 462 } 463 if (!isArmed() && timer.isRunning()) { 464 timer.stop(); 465 } 466 } 467 } 468 469 private static enum MenuIcon implements Icon { 470 471 UP(9, 1, 9), 472 DOWN(1, 9, 1); 473 static final int[] XPOINTS = {1, 5, 9}; 474 final int[] yPoints; 475 476 MenuIcon(int... yPoints) { 477 this.yPoints = yPoints; 478 } 479 480 @Override 481 public void paintIcon(Component c, Graphics g, int x, int y) { 482 Dimension size = c.getSize(); 483 Graphics g2 = g.create(size.width / 2 - 5, size.height / 2 - 5, 10, 10); 484 g2.setColor(Color.GRAY); 485 g2.drawPolygon(XPOINTS, yPoints, 3); 486 if (c.isEnabled()) { 487 g2.setColor(Color.BLACK); 488 g2.fillPolygon(XPOINTS, yPoints, 3); 489 } 490 g2.dispose(); 491 } 492 493 @Override 494 public int getIconWidth() { 495 return 0; 496 } 497 498 @Override 499 public int getIconHeight() { 500 return ARROW_ICON_HEIGHT; 501 } 502 } 503 504 private class MouseScrollListener implements MouseWheelListener { 505 @Override 506 public void mouseWheelMoved(MouseWheelEvent mwe) { 507 firstIndex += mwe.getWheelRotation(); 508 refreshMenu(); 509 mwe.consume(); 510 } 511 } 512}