001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2015 005 * Space Science and Engineering Center (SSEC) 006 * University of Wisconsin - Madison 007 * 1225 W. Dayton Street, Madison, WI 53706, USA 008 * https://www.ssec.wisc.edu/mcidas 009 * 010 * All Rights Reserved 011 * 012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and 013 * some McIDAS-V source code is based on IDV and VisAD source code. 014 * 015 * McIDAS-V is free software; you can redistribute it and/or modify 016 * it under the terms of the GNU Lesser Public License as published by 017 * the Free Software Foundation; either version 3 of the License, or 018 * (at your option) any later version. 019 * 020 * McIDAS-V is distributed in the hope that it will be useful, 021 * but WITHOUT ANY WARRANTY; without even the implied warranty of 022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 023 * GNU Lesser Public License for more details. 024 * 025 * You should have received a copy of the GNU Lesser Public License 026 * along with this program. If not, see http://www.gnu.org/licenses. 027 */ 028 029package edu.wisc.ssec.mcidasv.ui; 030 031import org.slf4j.Logger; 032import org.slf4j.LoggerFactory; 033 034import java.awt.Color; 035import java.awt.Component; 036import java.awt.Dimension; 037import java.awt.Graphics; 038 039import java.awt.Point; 040import java.awt.Toolkit; 041import java.awt.event.ActionEvent; 042import java.awt.event.ActionListener; 043 044import javax.swing.Icon; 045import javax.swing.JComponent; 046import javax.swing.JMenu; 047import javax.swing.JMenuItem; 048import javax.swing.JPopupMenu; 049import javax.swing.MenuSelectionManager; 050import javax.swing.SwingUtilities; 051import javax.swing.Timer; 052 053import javax.swing.event.ChangeEvent; 054import javax.swing.event.ChangeListener; 055import javax.swing.event.PopupMenuEvent; 056import javax.swing.event.PopupMenuListener; 057 058/** 059 * A class that provides scrolling capabilities to a long menu dropdown or 060 * popup menu. A number of items can optionally be frozen at the top and/or 061 * bottom of the menu. 062 * <p> 063 * <b>Implementation note:</b> The default number of items to display 064 * at a time is 15, and the default scrolling interval is 125 milliseconds. 065 * <p> 066 * This class is the work of Darryl Burke and the commenters at 067 * <a href=http://tips4java.wordpress.com/2009/02/01/menu-scroller/>this link</a>. 068 * 069 * @version 1.5.0 04/05/12 070 * @author Darryl 071 */ 072public class MenuScroller { 073 074 private static final Logger logger = LoggerFactory.getLogger(MenuScroller.class); 075 076 //private JMenu menu; 077 private JPopupMenu menu; 078 private Component[] menuItems; 079 private MenuScrollItem upItem; 080 private MenuScrollItem downItem; 081 private final MenuScrollListener menuListener = new MenuScrollListener(); 082 private int scrollCount; 083 private int interval; 084 private int topFixedCount; 085 private int bottomFixedCount; 086 private int firstIndex = 0; 087 private int keepVisibleIndex = -1; 088 089 /** 090 * Calculates the number for scrollCount such that the menu fills the available 091 * vertical space from the point (mouse press) to the bottom of the screen. 092 * 093 * @param c {@code Component} on which the point parameter is based. 094 * @param pt {@code Point} at which the top of the menu will appear (in component coordinate space). 095 * @param item {@code JMenuItem} of prototypical height off of which the average height is determined. 096 * @param bottomFixedCount Needed to offset the returned scrollCount. 097 * 098 * @return the {@literal "scrollCount"} for the given parameters. 099 */ 100 public static int scrollCountForScreen(Component c, Point pt, JMenuItem item, int bottomFixedCount) { 101 Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); 102 Point ptScreen = new Point(pt); 103 SwingUtilities.convertPointToScreen(ptScreen, c); 104 int height = screenSize.height - ptScreen.y; 105 int miHeight = item.getPreferredSize().height; 106 // 2 just takes the menu up a bit from the bottom which looks nicer 107 return (height / miHeight) - bottomFixedCount - 2; 108 } 109 110 /** 111 * Registers a menu to be scrolled with the default number of items to 112 * display at a time and the default scrolling interval. 113 * 114 * @param menu the menu 115 * @return the MenuScroller 116 */ 117 public static MenuScroller setScrollerFor(JMenu menu) { 118 return new MenuScroller(menu); 119 } 120 121 /** 122 * Registers a popup menu to be scrolled with the default number of items to 123 * display at a time and the default scrolling interval. 124 * 125 * @param menu the popup menu 126 * @return the MenuScroller 127 */ 128 public static MenuScroller setScrollerFor(JPopupMenu menu) { 129 return new MenuScroller(menu); 130 } 131 132 /** 133 * Registers a menu to be scrolled with the default number of items to 134 * display at a time and the specified scrolling interval. 135 * 136 * @param menu the menu 137 * @param scrollCount the number of items to display at a time 138 * @return the MenuScroller 139 * @throws IllegalArgumentException if scrollCount is 0 or negative 140 */ 141 public static MenuScroller setScrollerFor(JMenu menu, int scrollCount) { 142 return new MenuScroller(menu, scrollCount); 143 } 144 145 /** 146 * Registers a popup menu to be scrolled with the default number of items to 147 * display at a time and the specified scrolling interval. 148 * 149 * @param menu the popup menu 150 * @param scrollCount the number of items to display at a time 151 * @return the MenuScroller 152 * @throws IllegalArgumentException if scrollCount is 0 or negative 153 */ 154 public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount) { 155 return new MenuScroller(menu, scrollCount); 156 } 157 158 /** 159 * Registers a menu to be scrolled, with the specified number of items to 160 * display at a time and the specified scrolling interval. 161 * 162 * @param menu the menu 163 * @param scrollCount the number of items to be displayed at a time 164 * @param interval the scroll interval, in milliseconds 165 * @return the MenuScroller 166 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative 167 */ 168 public static MenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval) { 169 return new MenuScroller(menu, scrollCount, interval); 170 } 171 172 /** 173 * Registers a popup menu to be scrolled, with the specified number of items to 174 * display at a time and the specified scrolling interval. 175 * 176 * @param menu the popup menu 177 * @param scrollCount the number of items to be displayed at a time 178 * @param interval the scroll interval, in milliseconds 179 * @return the MenuScroller 180 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative 181 */ 182 public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount, int interval) { 183 return new MenuScroller(menu, scrollCount, interval); 184 } 185 186 /** 187 * Registers a menu to be scrolled, with the specified number of items 188 * to display in the scrolling region, the specified scrolling interval, 189 * and the specified numbers of items fixed at the top and bottom of the 190 * menu. 191 * 192 * @param menu the menu 193 * @param scrollCount the number of items to display in the scrolling portion 194 * @param interval the scroll interval, in milliseconds 195 * @param topFixedCount the number of items to fix at the top. May be 0. 196 * @param bottomFixedCount the number of items to fix at the bottom. May be 0 197 * @throws IllegalArgumentException if scrollCount or interval is 0 or 198 * negative or if topFixedCount or bottomFixedCount is negative 199 * @return the MenuScroller 200 */ 201 public static MenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval, 202 int topFixedCount, int bottomFixedCount) { 203 return new MenuScroller(menu, scrollCount, interval, 204 topFixedCount, bottomFixedCount); 205 } 206 207 /** 208 * Registers a popup menu to be scrolled, with the specified number of items 209 * to display in the scrolling region, the specified scrolling interval, 210 * and the specified numbers of items fixed at the top and bottom of the 211 * popup menu. 212 * 213 * @param menu the popup menu 214 * @param scrollCount the number of items to display in the scrolling portion 215 * @param interval the scroll interval, in milliseconds 216 * @param topFixedCount the number of items to fix at the top. May be 0 217 * @param bottomFixedCount the number of items to fix at the bottom. May be 0 218 * @throws IllegalArgumentException if scrollCount or interval is 0 or 219 * negative or if topFixedCount or bottomFixedCount is negative 220 * @return the MenuScroller 221 */ 222 public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount, int interval, 223 int topFixedCount, int bottomFixedCount) { 224 return new MenuScroller(menu, scrollCount, interval, 225 topFixedCount, bottomFixedCount); 226 } 227 228 /** 229 * Constructs a {@code MenuScroller} that scrolls a menu with the 230 * default number of items to display at a time, and default scrolling 231 * interval. 232 * 233 * @param menu the menu 234 */ 235 public MenuScroller(JMenu menu) { 236 this(menu, 15); 237 } 238 239 /** 240 * Constructs a {@code MenuScroller} that scrolls a popup menu with the 241 * default number of items to display at a time, and default scrolling 242 * interval. 243 * 244 * @param menu the popup menu 245 */ 246 public MenuScroller(JPopupMenu menu) { 247 this(menu, 15); 248 } 249 250 /** 251 * Constructs a {@code MenuScroller} that scrolls a menu with the 252 * specified number of items to display at a time, and default scrolling 253 * interval. 254 * 255 * @param menu the menu 256 * @param scrollCount the number of items to display at a time 257 * @throws IllegalArgumentException if scrollCount is 0 or negative 258 */ 259 public MenuScroller(JMenu menu, int scrollCount) { 260 this(menu, scrollCount, 150); 261 } 262 263 /** 264 * Constructs a {@code MenuScroller} that scrolls a popup menu with the 265 * specified number of items to display at a time, and default scrolling 266 * interval. 267 * 268 * @param menu the popup menu 269 * @param scrollCount the number of items to display at a time 270 * @throws IllegalArgumentException if scrollCount is 0 or negative 271 */ 272 public MenuScroller(JPopupMenu menu, int scrollCount) { 273 this(menu, scrollCount, 150); 274 } 275 276 /** 277 * Constructs a {@code MenuScroller} that scrolls a menu with the 278 * specified number of items to display at a time, and specified scrolling 279 * interval. 280 * 281 * @param menu the menu 282 * @param scrollCount the number of items to display at a time 283 * @param interval the scroll interval, in milliseconds 284 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative 285 */ 286 public MenuScroller(JMenu menu, int scrollCount, int interval) { 287 this(menu, scrollCount, interval, 0, 0); 288 } 289 290 /** 291 * Constructs a {@code MenuScroller} that scrolls a popup menu with the 292 * specified number of items to display at a time, and specified scrolling 293 * interval. 294 * 295 * @param menu the popup menu 296 * @param scrollCount the number of items to display at a time 297 * @param interval the scroll interval, in milliseconds 298 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative 299 */ 300 public MenuScroller(JPopupMenu menu, int scrollCount, int interval) { 301 this(menu, scrollCount, interval, 0, 0); 302 } 303 304 /** 305 * Constructs a {@code MenuScroller} that scrolls a menu with the 306 * specified number of items to display in the scrolling region, the 307 * specified scrolling interval, and the specified numbers of items fixed at 308 * the top and bottom of the menu. 309 * 310 * @param menu the menu 311 * @param scrollCount the number of items to display in the scrolling portion 312 * @param interval the scroll interval, in milliseconds 313 * @param topFixedCount the number of items to fix at the top. May be 0 314 * @param bottomFixedCount the number of items to fix at the bottom. May be 0 315 * @throws IllegalArgumentException if scrollCount or interval is 0 or 316 * negative or if topFixedCount or bottomFixedCount is negative 317 */ 318 public MenuScroller(JMenu menu, int scrollCount, int interval, 319 int topFixedCount, int bottomFixedCount) { 320 this(menu.getPopupMenu(), scrollCount, interval, topFixedCount, bottomFixedCount); 321 } 322 323 /** 324 * Constructs a {@code MenuScroller} that scrolls a popup menu with the 325 * specified number of items to display in the scrolling region, the 326 * specified scrolling interval, and the specified numbers of items fixed at 327 * the top and bottom of the popup menu. 328 * 329 * @param menu the popup menu 330 * @param scrollCount the number of items to display in the scrolling portion 331 * @param interval the scroll interval, in milliseconds 332 * @param topFixedCount the number of items to fix at the top. May be 0 333 * @param bottomFixedCount the number of items to fix at the bottom. May be 0 334 * @throws IllegalArgumentException if scrollCount or interval is 0 or 335 * negative or if topFixedCount or bottomFixedCount is negative 336 */ 337 public MenuScroller(JPopupMenu menu, int scrollCount, int interval, 338 int topFixedCount, int bottomFixedCount) { 339 if (scrollCount <= 0 || interval <= 0) { 340 throw new IllegalArgumentException("scrollCount and interval must be greater than 0"); 341 } 342 if (topFixedCount < 0 || bottomFixedCount < 0) { 343 throw new IllegalArgumentException("topFixedCount and bottomFixedCount cannot be negative"); 344 } 345 346 upItem = new MenuScrollItem(MenuIcon.UP, -1); 347 downItem = new MenuScrollItem(MenuIcon.DOWN, +1); 348 setScrollCount(scrollCount); 349 setInterval(interval); 350 setTopFixedCount(topFixedCount); 351 setBottomFixedCount(bottomFixedCount); 352 353 this.menu = menu; 354 menu.addPopupMenuListener(menuListener); 355 } 356 357 /** 358 * Returns the scroll interval in milliseconds 359 * 360 * @return the scroll interval in milliseconds 361 */ 362 public int getInterval() { 363 return interval; 364 } 365 366 /** 367 * Sets the scroll interval in milliseconds 368 * 369 * @param interval the scroll interval in milliseconds 370 * @throws IllegalArgumentException if interval is 0 or negative 371 */ 372 public void setInterval(int interval) { 373 if (interval <= 0) { 374 throw new IllegalArgumentException("interval must be greater than 0"); 375 } 376 upItem.setInterval(interval); 377 downItem.setInterval(interval); 378 this.interval = interval; 379 } 380 381 /** 382 * Returns the number of items in the scrolling portion of the menu. 383 * 384 * @return the number of items to display at a time 385 */ 386 public int getScrollCount() { 387 return scrollCount; 388 } 389 390 /** 391 * Sets the number of items in the scrolling portion of the menu. 392 * 393 * @param scrollCount the number of items to display at a time 394 * @throws IllegalArgumentException if scrollCount is 0 or negative 395 */ 396 public void setScrollCount(int scrollCount) { 397 if (scrollCount <= 0) { 398 throw new IllegalArgumentException("scrollCount must be greater than 0"); 399 } 400 this.scrollCount = scrollCount; 401 MenuSelectionManager.defaultManager().clearSelectedPath(); 402 } 403 404 /** 405 * Returns the number of items fixed at the top of the menu or popup menu. 406 * 407 * @return the number of items 408 */ 409 public int getTopFixedCount() { 410 return topFixedCount; 411 } 412 413 /** 414 * Sets the number of items to fix at the top of the menu or popup menu. 415 * 416 * @param topFixedCount the number of items 417 */ 418 public void setTopFixedCount(int topFixedCount) { 419 if (firstIndex <= topFixedCount) { 420 firstIndex = topFixedCount; 421 } else { 422 firstIndex += (topFixedCount - this.topFixedCount); 423 } 424 this.topFixedCount = topFixedCount; 425 } 426 427 /** 428 * Returns the number of items fixed at the bottom of the menu or popup menu. 429 * 430 * @return the number of items 431 */ 432 public int getBottomFixedCount() { 433 return bottomFixedCount; 434 } 435 436 /** 437 * Sets the number of items to fix at the bottom of the menu or popup menu. 438 * 439 * @param bottomFixedCount the number of items 440 */ 441 public void setBottomFixedCount(int bottomFixedCount) { 442 this.bottomFixedCount = bottomFixedCount; 443 } 444 445 /** 446 * Scrolls the specified item into view each time the menu is opened. Call this method with 447 * {@code null} to restore the default behavior, which is to show the menu as it last 448 * appeared. 449 * 450 * @param item the item to keep visible 451 * @see #keepVisible(int) 452 */ 453 public void keepVisible(JMenuItem item) { 454 if (item == null) { 455 keepVisibleIndex = -1; 456 } else { 457 int index = menu.getComponentIndex(item); 458 keepVisibleIndex = index; 459 } 460 } 461 462 /** 463 * Scrolls the item at the specified index into view each time the menu is opened. Call this 464 * method with {@code -1} to restore the default behavior, which is to show the menu as 465 * it last appeared. 466 * 467 * @param index the index of the item to keep visible 468 * @see #keepVisible(javax.swing.JMenuItem) 469 */ 470 public void keepVisible(int index) { 471 keepVisibleIndex = index; 472 } 473 474 /** 475 * Removes this MenuScroller from the associated menu and restores the 476 * default behavior of the menu. 477 */ 478 public void dispose() { 479 if (menu != null) { 480 menu.removePopupMenuListener(menuListener); 481 menu = null; 482 } 483 } 484 485 /** 486 * Ensures that the {@code dispose} method of this MenuScroller is 487 * called when there are no more refrences to it. 488 * 489 * @exception Throwable if an error occurs. 490 * @see MenuScroller#dispose() 491 */ 492 @Override public void finalize() throws Throwable { 493 dispose(); 494 } 495 496 private void refreshMenu() { 497 if (menuItems != null && menuItems.length > 0) { 498 firstIndex = Math.max(topFixedCount, firstIndex); 499 firstIndex = Math.min(menuItems.length - bottomFixedCount - scrollCount, firstIndex); 500 501 upItem.setEnabled(firstIndex > topFixedCount); 502 downItem.setEnabled(firstIndex + scrollCount < menuItems.length - bottomFixedCount); 503 504 menu.removeAll(); 505 for (int i = 0; i < topFixedCount; i++) { 506 menu.add(menuItems[i]); 507 } 508 if (topFixedCount > 0) { 509 menu.addSeparator(); 510 } 511 512 menu.add(upItem); 513 for (int i = firstIndex; i < scrollCount + firstIndex; i++) { 514 menu.add(menuItems[i]); 515 } 516 menu.add(downItem); 517 518 if (bottomFixedCount > 0) { 519 menu.addSeparator(); 520 } 521 for (int i = menuItems.length - bottomFixedCount; i < menuItems.length; i++) { 522 menu.add(menuItems[i]); 523 } 524 525 int preferredWidth = 0; 526 for (Component item : menuItems) { 527 preferredWidth = Math.max(preferredWidth, item.getPreferredSize().width); 528 } 529 menu.setPreferredSize(new Dimension(preferredWidth, menu.getPreferredSize().height)); 530 531 JComponent parent = (JComponent)upItem.getParent(); 532 parent.revalidate(); 533 parent.repaint(); 534 Component invoker = menu.getInvoker(); 535 Dimension invokerSize = invoker.getSize(); 536 Point invokerLocation = invoker.getLocationOnScreen(); 537 int menuX = (int)(invokerSize.getWidth() + invokerLocation.getX()); 538 Point newMenuLocation = new Point(menuX, 1000); 539 if (!menu.isVisible()) { 540 menu.setLocation(newMenuLocation); 541 } 542 } 543 } 544 545 private class MenuScrollListener implements PopupMenuListener { 546 547 @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { 548 setMenuItems(); 549// logger.trace("e={}", e); 550 } 551 552 @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { 553 restoreMenuItems(); 554 } 555 556 @Override public void popupMenuCanceled(PopupMenuEvent e) { 557 restoreMenuItems(); 558 } 559 560 private void setMenuItems() { 561 menuItems = menu.getComponents(); 562 if (keepVisibleIndex >= topFixedCount 563 && keepVisibleIndex <= menuItems.length - bottomFixedCount 564 && (keepVisibleIndex > firstIndex + scrollCount 565 || keepVisibleIndex < firstIndex)) { 566 firstIndex = Math.min(firstIndex, keepVisibleIndex); 567 firstIndex = Math.max(firstIndex, keepVisibleIndex - scrollCount + 1); 568 } 569 if (menuItems.length > topFixedCount + scrollCount + bottomFixedCount) { 570 refreshMenu(); 571 } 572 } 573 574 private void restoreMenuItems() { 575 menu.removeAll(); 576 for (Component component : menuItems) { 577 menu.add(component); 578 } 579 } 580 } 581 582 private class MenuScrollTimer extends Timer { 583 584 public MenuScrollTimer(final int increment, int interval) { 585 super(interval, new ActionListener() { 586 587 @Override public void actionPerformed(ActionEvent e) { 588 firstIndex += increment; 589 refreshMenu(); 590 } 591 }); 592 } 593 } 594 595 private class MenuScrollItem extends JMenuItem 596 implements ChangeListener { 597 598 private MenuScrollTimer timer; 599 600 public MenuScrollItem(MenuIcon icon, int increment) { 601 setIcon(icon); 602 setDisabledIcon(icon); 603 timer = new MenuScrollTimer(increment, interval); 604 addChangeListener(this); 605 } 606 607 public void setInterval(int interval) { 608 timer.setDelay(interval); 609 } 610 611 @Override public void stateChanged(ChangeEvent e) { 612 if (isArmed() && !timer.isRunning()) { 613 timer.start(); 614 } 615 if (!isArmed() && timer.isRunning()) { 616 timer.stop(); 617 } 618 } 619 } 620 621 private static enum MenuIcon implements Icon { 622 623 UP(9, 1, 9), 624 DOWN(1, 9, 1); 625 final int[] xPoints = {1, 5, 9}; 626 final int[] yPoints; 627 628 MenuIcon(int... yPoints) { 629 this.yPoints = yPoints; 630 } 631 632 @Override public void paintIcon(Component c, Graphics g, int x, int y) { 633 Dimension size = c.getSize(); 634 Graphics g2 = g.create(size.width / 2 - 5, size.height / 2 - 5, 10, 10); 635 g2.setColor(Color.GRAY); 636 g2.drawPolygon(xPoints, yPoints, 3); 637 if (c.isEnabled()) { 638 g2.setColor(Color.BLACK); 639 g2.fillPolygon(xPoints, yPoints, 3); 640 } 641 g2.dispose(); 642 } 643 644 @Override public int getIconWidth() { 645 return 0; 646 } 647 648 @Override public int getIconHeight() { 649 return 10; 650 } 651 } 652}