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