001/** 002 * MenuScroller.java 1.5.0 04/02/12 003 * License: use / modify without restrictions (see http://tips4java.wordpress.com/about/) 004 */ 005package org.openstreetmap.josm.gui; 006 007import java.awt.Color; 008import java.awt.Component; 009import java.awt.Dimension; 010import java.awt.Graphics; 011import java.awt.event.ActionEvent; 012import java.awt.event.ActionListener; 013import java.awt.event.MouseWheelEvent; 014import java.awt.event.MouseWheelListener; 015 016import javax.swing.Icon; 017import javax.swing.JComponent; 018import javax.swing.JMenu; 019import javax.swing.JMenuItem; 020import javax.swing.JPopupMenu; 021import javax.swing.MenuSelectionManager; 022import javax.swing.Timer; 023import javax.swing.event.ChangeEvent; 024import javax.swing.event.ChangeListener; 025import javax.swing.event.PopupMenuEvent; 026import javax.swing.event.PopupMenuListener; 027 028/** 029 * A class that provides scrolling capabilities to a long menu dropdown or 030 * popup menu. A number of items can optionally be frozen at the top and/or 031 * bottom of the menu. 032 * <P> 033 * <B>Implementation note:</B> The default number of items to display 034 * at a time is 15, and the default scrolling interval is 125 milliseconds. 035 * <P> 036 * @author Darryl, http://tips4java.wordpress.com/2009/02/01/menu-scroller/ 037 */ 038public class MenuScroller { 039 040 private JPopupMenu menu; 041 private Component[] menuItems; 042 private MenuScrollItem upItem; 043 private MenuScrollItem downItem; 044 private final MenuScrollListener menuListener = new MenuScrollListener(); 045 private final MouseWheelListener mouseWheelListener = new MouseScrollListener(); 046 private int scrollCount; 047 private int interval; 048 private int topFixedCount; 049 private int bottomFixedCount; 050 private int firstIndex = 0; 051 private int keepVisibleIndex = -1; 052 053 /** 054 * Registers a menu to be scrolled with the default number of items to 055 * display at a time and the default scrolling interval. 056 * 057 * @param menu the menu 058 * @return the MenuScroller 059 */ 060 public static MenuScroller setScrollerFor(JMenu menu) { 061 return new MenuScroller(menu); 062 } 063 064 /** 065 * Registers a popup menu to be scrolled with the default number of items to 066 * display at a time and the default scrolling interval. 067 * 068 * @param menu the popup menu 069 * @return the MenuScroller 070 */ 071 public static MenuScroller setScrollerFor(JPopupMenu menu) { 072 return new MenuScroller(menu); 073 } 074 075 /** 076 * Registers a menu to be scrolled with the default number of items to 077 * display at a time and the specified scrolling interval. 078 * 079 * @param menu the menu 080 * @param scrollCount the number of items to display at a time 081 * @return the MenuScroller 082 * @throws IllegalArgumentException if scrollCount is 0 or negative 083 */ 084 public static MenuScroller setScrollerFor(JMenu menu, int scrollCount) { 085 return new MenuScroller(menu, scrollCount); 086 } 087 088 /** 089 * Registers a popup menu to be scrolled with the default number of items to 090 * display at a time and the specified scrolling interval. 091 * 092 * @param menu the popup menu 093 * @param scrollCount the number of items to display at a time 094 * @return the MenuScroller 095 * @throws IllegalArgumentException if scrollCount is 0 or negative 096 */ 097 public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount) { 098 return new MenuScroller(menu, scrollCount); 099 } 100 101 /** 102 * Registers a menu to be scrolled, with the specified number of items to 103 * display at a time and the specified scrolling interval. 104 * 105 * @param menu the menu 106 * @param scrollCount the number of items to be displayed at a time 107 * @param interval the scroll interval, in milliseconds 108 * @return the MenuScroller 109 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative 110 */ 111 public static MenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval) { 112 return new MenuScroller(menu, scrollCount, interval); 113 } 114 115 /** 116 * Registers a popup menu to be scrolled, with the specified number of items to 117 * display at a time and the specified scrolling interval. 118 * 119 * @param menu the popup menu 120 * @param scrollCount the number of items to be displayed at a time 121 * @param interval the scroll interval, in milliseconds 122 * @return the MenuScroller 123 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative 124 */ 125 public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount, int interval) { 126 return new MenuScroller(menu, scrollCount, interval); 127 } 128 129 /** 130 * Registers a menu to be scrolled, with the specified number of items 131 * to display in the scrolling region, the specified scrolling interval, 132 * and the specified numbers of items fixed at the top and bottom of the 133 * menu. 134 * 135 * @param menu the menu 136 * @param scrollCount the number of items to display in the scrolling portion 137 * @param interval the scroll interval, in milliseconds 138 * @param topFixedCount the number of items to fix at the top. May be 0. 139 * @param bottomFixedCount the number of items to fix at the bottom. May be 0 140 * @throws IllegalArgumentException if scrollCount or interval is 0 or 141 * negative or if topFixedCount or bottomFixedCount is negative 142 * @return the MenuScroller 143 */ 144 public static MenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval, 145 int topFixedCount, int bottomFixedCount) { 146 return new MenuScroller(menu, scrollCount, interval, 147 topFixedCount, bottomFixedCount); 148 } 149 150 /** 151 * Registers a popup menu to be scrolled, with the specified number of items 152 * to display in the scrolling region, the specified scrolling interval, 153 * and the specified numbers of items fixed at the top and bottom of the 154 * popup menu. 155 * 156 * @param menu the popup menu 157 * @param scrollCount the number of items to display in the scrolling portion 158 * @param interval the scroll interval, in milliseconds 159 * @param topFixedCount the number of items to fix at the top. May be 0 160 * @param bottomFixedCount the number of items to fix at the bottom. May be 0 161 * @throws IllegalArgumentException if scrollCount or interval is 0 or 162 * negative or if topFixedCount or bottomFixedCount is negative 163 * @return the MenuScroller 164 */ 165 public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount, int interval, 166 int topFixedCount, int bottomFixedCount) { 167 return new MenuScroller(menu, scrollCount, interval, 168 topFixedCount, bottomFixedCount); 169 } 170 171 /** 172 * Constructs a <code>MenuScroller</code> that scrolls a menu with the 173 * default number of items to display at a time, and default scrolling 174 * interval. 175 * 176 * @param menu the menu 177 */ 178 public MenuScroller(JMenu menu) { 179 this(menu, 15); 180 } 181 182 /** 183 * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the 184 * default number of items to display at a time, and default scrolling 185 * interval. 186 * 187 * @param menu the popup menu 188 */ 189 public MenuScroller(JPopupMenu menu) { 190 this(menu, 15); 191 } 192 193 /** 194 * Constructs a <code>MenuScroller</code> that scrolls a menu with the 195 * specified number of items to display at a time, and default scrolling 196 * interval. 197 * 198 * @param menu the menu 199 * @param scrollCount the number of items to display at a time 200 * @throws IllegalArgumentException if scrollCount is 0 or negative 201 */ 202 public MenuScroller(JMenu menu, int scrollCount) { 203 this(menu, scrollCount, 150); 204 } 205 206 /** 207 * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the 208 * specified number of items to display at a time, and default scrolling 209 * interval. 210 * 211 * @param menu the popup menu 212 * @param scrollCount the number of items to display at a time 213 * @throws IllegalArgumentException if scrollCount is 0 or negative 214 */ 215 public MenuScroller(JPopupMenu menu, int scrollCount) { 216 this(menu, scrollCount, 150); 217 } 218 219 /** 220 * Constructs a <code>MenuScroller</code> that scrolls a menu with the 221 * specified number of items to display at a time, and specified scrolling 222 * interval. 223 * 224 * @param menu the menu 225 * @param scrollCount the number of items to display at a time 226 * @param interval the scroll interval, in milliseconds 227 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative 228 */ 229 public MenuScroller(JMenu menu, int scrollCount, int interval) { 230 this(menu, scrollCount, interval, 0, 0); 231 } 232 233 /** 234 * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the 235 * specified number of items to display at a time, and specified scrolling 236 * interval. 237 * 238 * @param menu the popup menu 239 * @param scrollCount the number of items to display at a time 240 * @param interval the scroll interval, in milliseconds 241 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative 242 */ 243 public MenuScroller(JPopupMenu menu, int scrollCount, int interval) { 244 this(menu, scrollCount, interval, 0, 0); 245 } 246 247 /** 248 * Constructs a <code>MenuScroller</code> that scrolls a menu with the 249 * specified number of items to display in the scrolling region, the 250 * specified scrolling interval, and the specified numbers of items fixed at 251 * the top and bottom of the menu. 252 * 253 * @param menu the menu 254 * @param scrollCount the number of items to display in the scrolling portion 255 * @param interval the scroll interval, in milliseconds 256 * @param topFixedCount the number of items to fix at the top. May be 0 257 * @param bottomFixedCount the number of items to fix at the bottom. May be 0 258 * @throws IllegalArgumentException if scrollCount or interval is 0 or 259 * negative or if topFixedCount or bottomFixedCount is negative 260 */ 261 public MenuScroller(JMenu menu, int scrollCount, int interval, 262 int topFixedCount, int bottomFixedCount) { 263 this(menu.getPopupMenu(), scrollCount, interval, topFixedCount, bottomFixedCount); 264 } 265 266 /** 267 * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the 268 * specified number of items to display in the scrolling region, the 269 * specified scrolling interval, and the specified numbers of items fixed at 270 * the top and bottom of the popup menu. 271 * 272 * @param menu the popup menu 273 * @param scrollCount the number of items to display in the scrolling portion 274 * @param interval the scroll interval, in milliseconds 275 * @param topFixedCount the number of items to fix at the top. May be 0 276 * @param bottomFixedCount the number of items to fix at the bottom. May be 0 277 * @throws IllegalArgumentException if scrollCount or interval is 0 or 278 * negative or if topFixedCount or bottomFixedCount is negative 279 */ 280 public MenuScroller(JPopupMenu menu, int scrollCount, int interval, 281 int topFixedCount, int bottomFixedCount) { 282 if (scrollCount <= 0 || interval <= 0) { 283 throw new IllegalArgumentException("scrollCount and interval must be greater than 0"); 284 } 285 if (topFixedCount < 0 || bottomFixedCount < 0) { 286 throw new IllegalArgumentException("topFixedCount and bottomFixedCount cannot be negative"); 287 } 288 289 upItem = new MenuScrollItem(MenuIcon.UP, -1); 290 downItem = new MenuScrollItem(MenuIcon.DOWN, +1); 291 setScrollCount(scrollCount); 292 setInterval(interval); 293 setTopFixedCount(topFixedCount); 294 setBottomFixedCount(bottomFixedCount); 295 296 this.menu = menu; 297 menu.addPopupMenuListener(menuListener); 298 menu.addMouseWheelListener(mouseWheelListener); 299 } 300 301 /** 302 * Returns the scroll interval in milliseconds 303 * 304 * @return the scroll interval in milliseconds 305 */ 306 public int getInterval() { 307 return interval; 308 } 309 310 /** 311 * Sets the scroll interval in milliseconds 312 * 313 * @param interval the scroll interval in milliseconds 314 * @throws IllegalArgumentException if interval is 0 or negative 315 */ 316 public void setInterval(int interval) { 317 if (interval <= 0) { 318 throw new IllegalArgumentException("interval must be greater than 0"); 319 } 320 upItem.setInterval(interval); 321 downItem.setInterval(interval); 322 this.interval = interval; 323 } 324 325 /** 326 * Returns the number of items in the scrolling portion of the menu. 327 * 328 * @return the number of items to display at a time 329 */ 330 public int getscrollCount() { 331 return scrollCount; 332 } 333 334 /** 335 * Sets the number of items in the scrolling portion of the menu. 336 * 337 * @param scrollCount the number of items to display at a time 338 * @throws IllegalArgumentException if scrollCount is 0 or negative 339 */ 340 public void setScrollCount(int scrollCount) { 341 if (scrollCount <= 0) { 342 throw new IllegalArgumentException("scrollCount must be greater than 0"); 343 } 344 this.scrollCount = scrollCount; 345 MenuSelectionManager.defaultManager().clearSelectedPath(); 346 } 347 348 /** 349 * Returns the number of items fixed at the top of the menu or popup menu. 350 * 351 * @return the number of items 352 */ 353 public int getTopFixedCount() { 354 return topFixedCount; 355 } 356 357 /** 358 * Sets the number of items to fix at the top of the menu or popup menu. 359 * 360 * @param topFixedCount the number of items 361 */ 362 public void setTopFixedCount(int topFixedCount) { 363 if (firstIndex <= topFixedCount) { 364 firstIndex = topFixedCount; 365 } else { 366 firstIndex += (topFixedCount - this.topFixedCount); 367 } 368 this.topFixedCount = topFixedCount; 369 } 370 371 /** 372 * Returns the number of items fixed at the bottom of the menu or popup menu. 373 * 374 * @return the number of items 375 */ 376 public int getBottomFixedCount() { 377 return bottomFixedCount; 378 } 379 380 /** 381 * Sets the number of items to fix at the bottom of the menu or popup menu. 382 * 383 * @param bottomFixedCount the number of items 384 */ 385 public void setBottomFixedCount(int bottomFixedCount) { 386 this.bottomFixedCount = bottomFixedCount; 387 } 388 389 /** 390 * Scrolls the specified item into view each time the menu is opened. Call this method with 391 * <code>null</code> to restore the default behavior, which is to show the menu as it last 392 * appeared. 393 * 394 * @param item the item to keep visible 395 * @see #keepVisible(int) 396 */ 397 public void keepVisible(JMenuItem item) { 398 if (item == null) { 399 keepVisibleIndex = -1; 400 } else { 401 int index = menu.getComponentIndex(item); 402 keepVisibleIndex = index; 403 } 404 } 405 406 /** 407 * Scrolls the item at the specified index into view each time the menu is opened. Call this 408 * method with <code>-1</code> to restore the default behavior, which is to show the menu as 409 * it last appeared. 410 * 411 * @param index the index of the item to keep visible 412 * @see #keepVisible(javax.swing.JMenuItem) 413 */ 414 public void keepVisible(int index) { 415 keepVisibleIndex = index; 416 } 417 418 /** 419 * Removes this MenuScroller from the associated menu and restores the 420 * default behavior of the menu. 421 */ 422 public void dispose() { 423 if (menu != null) { 424 menu.removePopupMenuListener(menuListener); 425 menu.removeMouseWheelListener(mouseWheelListener); 426 menu.setPreferredSize(null); 427 menu = null; 428 } 429 } 430 431 /** 432 * Ensures that the <code>dispose</code> method of this MenuScroller is 433 * called when there are no more refrences to it. 434 * 435 * @exception Throwable if an error occurs. 436 * @see MenuScroller#dispose() 437 */ 438 @Override 439 protected void finalize() throws Throwable { 440 dispose(); 441 } 442 443 private void refreshMenu() { 444 if (menuItems != null && menuItems.length > 0) { 445 firstIndex = Math.max(topFixedCount, firstIndex); 446 firstIndex = Math.min(menuItems.length - bottomFixedCount - scrollCount, firstIndex); 447 448 upItem.setEnabled(firstIndex > topFixedCount); 449 downItem.setEnabled(firstIndex + scrollCount < menuItems.length - bottomFixedCount); 450 451 menu.removeAll(); 452 for (int i = 0; i < topFixedCount; i++) { 453 menu.add(menuItems[i]); 454 } 455 if (topFixedCount > 0) { 456 menu.addSeparator(); 457 } 458 459 menu.add(upItem); 460 for (int i = firstIndex; i < scrollCount + firstIndex; i++) { 461 menu.add(menuItems[i]); 462 } 463 menu.add(downItem); 464 465 if (bottomFixedCount > 0) { 466 menu.addSeparator(); 467 } 468 for (int i = menuItems.length - bottomFixedCount; i < menuItems.length; i++) { 469 menu.add(menuItems[i]); 470 } 471 472 int preferredWidth = 0; 473 for (Component item : menuItems) { 474 preferredWidth = Math.max(preferredWidth, item.getPreferredSize().width); 475 } 476 menu.setPreferredSize(new Dimension(preferredWidth, menu.getPreferredSize().height)); 477 478 JComponent parent = (JComponent) upItem.getParent(); 479 parent.revalidate(); 480 parent.repaint(); 481 } 482 } 483 484 private class MenuScrollListener implements PopupMenuListener { 485 486 @Override 487 public void popupMenuWillBecomeVisible(PopupMenuEvent e) { 488 setMenuItems(); 489 } 490 491 @Override 492 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { 493 restoreMenuItems(); 494 } 495 496 @Override 497 public void popupMenuCanceled(PopupMenuEvent e) { 498 restoreMenuItems(); 499 } 500 501 private void setMenuItems() { 502 menuItems = menu.getComponents(); 503 if (keepVisibleIndex >= topFixedCount 504 && keepVisibleIndex <= menuItems.length - bottomFixedCount 505 && (keepVisibleIndex > firstIndex + scrollCount 506 || keepVisibleIndex < firstIndex)) { 507 firstIndex = Math.min(firstIndex, keepVisibleIndex); 508 firstIndex = Math.max(firstIndex, keepVisibleIndex - scrollCount + 1); 509 } 510 if (menuItems.length > topFixedCount + scrollCount + bottomFixedCount) { 511 refreshMenu(); 512 } 513 } 514 515 private void restoreMenuItems() { 516 menu.removeAll(); 517 for (Component component : menuItems) { 518 menu.add(component); 519 } 520 } 521 } 522 523 private class MenuScrollTimer extends Timer { 524 525 public MenuScrollTimer(final int increment, int interval) { 526 super(interval, new ActionListener() { 527 528 @Override 529 public void actionPerformed(ActionEvent e) { 530 firstIndex += increment; 531 refreshMenu(); 532 } 533 }); 534 } 535 } 536 537 private class MenuScrollItem extends JMenuItem 538 implements ChangeListener { 539 540 private MenuScrollTimer timer; 541 542 public MenuScrollItem(MenuIcon icon, int increment) { 543 setIcon(icon); 544 setDisabledIcon(icon); 545 timer = new MenuScrollTimer(increment, interval); 546 addChangeListener(this); 547 } 548 549 public void setInterval(int interval) { 550 timer.setDelay(interval); 551 } 552 553 @Override 554 public void stateChanged(ChangeEvent e) { 555 if (isArmed() && !timer.isRunning()) { 556 timer.start(); 557 } 558 if (!isArmed() && timer.isRunning()) { 559 timer.stop(); 560 } 561 } 562 } 563 564 private static enum MenuIcon implements Icon { 565 566 UP(9, 1, 9), 567 DOWN(1, 9, 1); 568 final int[] xPoints = {1, 5, 9}; 569 final int[] yPoints; 570 571 MenuIcon(int... yPoints) { 572 this.yPoints = yPoints; 573 } 574 575 @Override 576 public void paintIcon(Component c, Graphics g, int x, int y) { 577 Dimension size = c.getSize(); 578 Graphics g2 = g.create(size.width / 2 - 5, size.height / 2 - 5, 10, 10); 579 g2.setColor(Color.GRAY); 580 g2.drawPolygon(xPoints, yPoints, 3); 581 if (c.isEnabled()) { 582 g2.setColor(Color.BLACK); 583 g2.fillPolygon(xPoints, yPoints, 3); 584 } 585 g2.dispose(); 586 } 587 588 @Override 589 public int getIconWidth() { 590 return 0; 591 } 592 593 @Override 594 public int getIconHeight() { 595 return 10; 596 } 597 } 598 599 private class MouseScrollListener implements MouseWheelListener { 600 @Override 601 public void mouseWheelMoved(MouseWheelEvent mwe) { 602 if (menu.getComponents().length > scrollCount) { 603 firstIndex += mwe.getWheelRotation(); 604 refreshMenu(); 605 } 606 mwe.consume(); // (Comment 16, Huw) 607 } 608 } 609}