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