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