001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2016 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 java.awt.Color; 032import java.awt.Component; 033import java.awt.Dimension; 034import java.awt.Graphics; 035import java.awt.GraphicsConfiguration; 036import java.awt.Insets; 037import java.awt.Rectangle; 038import java.awt.event.ActionEvent; 039import java.awt.event.ActionListener; 040import java.awt.event.MouseWheelEvent; 041import java.awt.event.MouseWheelListener; 042import java.util.Arrays; 043import java.util.Objects; 044 045import javax.swing.Icon; 046import javax.swing.JComponent; 047import javax.swing.JMenu; 048import javax.swing.JMenuItem; 049import javax.swing.JPopupMenu; 050import javax.swing.JSeparator; 051import javax.swing.Timer; 052import javax.swing.event.ChangeEvent; 053import javax.swing.event.ChangeListener; 054import javax.swing.event.PopupMenuEvent; 055import javax.swing.event.PopupMenuListener; 056 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 of the menu. 061 * <p> 062 * <b>Implementation note:</b> The default scrolling interval is 150 milliseconds. 063 * <p> 064 * @author Darryl, https://tips4java.wordpress.com/2009/02/01/menu-scroller/ 065 * @since 4593 066 * 067 * MenuScroller.java 1.5.0 04/02/12 068 * License: use / modify without restrictions (see https://tips4java.wordpress.com/about/) 069 * Heavily modified for JOSM needs => drop unused features and replace static scrollcount approach by dynamic behaviour 070 */ 071public class MenuScroller { 072 073 private JComponent parent; 074 private JPopupMenu menu; 075 private Component[] menuItems; 076 private MenuScrollItem upItem; 077 private MenuScrollItem downItem; 078 private final MenuScrollListener menuListener = new MenuScrollListener(); 079 private final MouseWheelListener mouseWheelListener = new MouseScrollListener(); 080 private int interval; 081 private int topFixedCount; 082 private int firstIndex = 0; 083 084 private static final int ARROW_ICON_HEIGHT = 10; 085 086 /** 087 * Computes the maximum dimension for a component to fit in screen 088 * displaying {@code component}. 089 * 090 * @param component The component to get current screen info from. 091 * Must not be {@code null} 092 * 093 * @return Maximum dimension for a component to fit in current screen. 094 * 095 * @throws NullPointerException if {@code component} is {@code null}. 096 */ 097 public static Dimension getMaxDimensionOnScreen(JComponent parent, JComponent component) { 098 Objects.requireNonNull(component, "component"); 099 // Compute max dimension of current screen 100 Dimension result = new Dimension(); 101 GraphicsConfiguration gc = component.getGraphicsConfiguration(); 102 if ((gc == null) && (parent != null)) { 103 gc = parent.getGraphicsConfiguration(); 104 } 105 if (gc != null) { 106 // Max displayable dimension (max screen dimension - insets) 107 Rectangle bounds = gc.getBounds(); 108 Insets insets = component.getToolkit().getScreenInsets(gc); 109 result.width = bounds.width - insets.left - insets.right; 110 result.height = bounds.height - insets.top - insets.bottom; 111 } 112 return result; 113 } 114 115 private int computeScrollCount(int startIndex) { 116 int result = 15; 117 if (menu != null) { 118 // Compute max height of current screen 119// Component parent = IdvWindow.getActiveWindow().getFrame(); 120 int maxHeight = getMaxDimensionOnScreen(parent, menu).height - parent.getInsets().top; 121 122 // Remove top fixed part height 123 if (topFixedCount > 0) { 124 for (int i = 0; i < topFixedCount; i++) { 125 maxHeight -= menuItems[i].getPreferredSize().height; 126 } 127 maxHeight -= new JSeparator().getPreferredSize().height; 128 } 129 130 // Remove height of our two arrow items + insets 131 maxHeight -= menu.getInsets().top; 132 maxHeight -= upItem.getPreferredSize().height; 133 maxHeight -= downItem.getPreferredSize().height; 134 maxHeight -= menu.getInsets().bottom; 135 136 // Compute scroll count 137 result = 0; 138 int height = 0; 139 for (int i = startIndex; (i < menuItems.length) && (height <= maxHeight); i++, result++) { 140 height += menuItems[i].getPreferredSize().height; 141 } 142 143 if (height > maxHeight) { 144 // Remove extra item from count 145 result--; 146 } else { 147 // Increase scroll count to take into account upper items that will be displayed 148 // after firstIndex is updated 149 for (int i = startIndex-1; (i >= 0) && (height <= maxHeight); i--, result++) { 150 height += menuItems[i].getPreferredSize().height; 151 } 152 if (height > maxHeight) { 153 result--; 154 } 155 } 156 } 157 return result; 158 } 159 160 /** 161 * Registers a menu to be scrolled with the default scrolling interval. 162 * 163 * @param menu Menu to 164 * @return the MenuScroller 165 */ 166 public static MenuScroller setScrollerFor(JMenu menu) { 167 return new MenuScroller(menu); 168 } 169 170 /** 171 * Registers a popup menu to be scrolled with the default scrolling interval. 172 * 173 * @param menu the popup menu 174 * @return the MenuScroller 175 */ 176 public static MenuScroller setScrollerFor(JPopupMenu menu) { 177 return new MenuScroller(menu); 178 } 179 180 /** 181 * Registers a menu to be scrolled, with the specified scrolling interval. 182 * 183 * @param menu the menu 184 * @param interval the scroll interval, in milliseconds 185 * @return the MenuScroller 186 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative 187 */ 188 public static MenuScroller setScrollerFor(JMenu menu, int interval) { 189 return new MenuScroller(menu, interval); 190 } 191 192 /** 193 * Registers a popup menu to be scrolled, with the specified scrolling interval. 194 * 195 * @param menu the popup menu 196 * @param interval the scroll interval, in milliseconds 197 * @return the MenuScroller 198 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative 199 */ 200 public static MenuScroller setScrollerFor(JPopupMenu menu, int interval) { 201 return new MenuScroller(menu, interval); 202 } 203 204 /** 205 * Registers a menu to be scrolled, with the specified scrolling interval, 206 * and the specified numbers of items fixed at the top of the menu. 207 * 208 * @param menu the menu 209 * @param interval the scroll interval, in milliseconds 210 * @param topFixedCount the number of items to fix at the top. May be 0. 211 * @throws IllegalArgumentException if scrollCount or interval is 0 or 212 * negative or if topFixedCount is negative 213 * @return the MenuScroller 214 */ 215 public static MenuScroller setScrollerFor(JMenu menu, int interval, int topFixedCount) { 216 return new MenuScroller(menu, interval, topFixedCount); 217 } 218 219 /** 220 * Registers a popup menu to be scrolled, with the specified scrolling interval, 221 * and the specified numbers of items fixed at the top of the popup menu. 222 * 223 * @param menu the popup menu 224 * @param interval the scroll interval, in milliseconds 225 * @param topFixedCount the number of items to fix at the top. May be 0 226 * @throws IllegalArgumentException if scrollCount or interval is 0 or 227 * negative or if topFixedCount is negative 228 * @return the MenuScroller 229 */ 230 public static MenuScroller setScrollerFor(JPopupMenu menu, int interval, int topFixedCount) { 231 return new MenuScroller(menu, interval, topFixedCount); 232 } 233 234 /** 235 * Constructs a {@code MenuScroller} that scrolls a menu with the 236 * default scrolling interval. 237 * 238 * @param menu the menu 239 * @throws IllegalArgumentException if scrollCount is 0 or negative 240 */ 241 public MenuScroller(JMenu menu) { 242 this(menu, 150); 243 } 244 245 public MenuScroller(JComponent parentComp, JMenu menu) { 246 this(menu, 150); 247 parent = parentComp; 248 } 249 250 /** 251 * Constructs a {@code MenuScroller} that scrolls a popup menu with the 252 * default scrolling interval. 253 * 254 * @param menu the popup menu 255 * @throws IllegalArgumentException if scrollCount is 0 or negative 256 */ 257 public MenuScroller(JPopupMenu menu) { 258 this(menu, 150); 259 } 260 261 /** 262 * Constructs a {@code MenuScroller} that scrolls a menu with the 263 * specified scrolling interval. 264 * 265 * @param menu the menu 266 * @param interval the scroll interval, in milliseconds 267 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative 268 */ 269 public MenuScroller(JMenu menu, int interval) { 270 this(menu, interval, 0); 271 } 272 273 /** 274 * Constructs a {@code MenuScroller} that scrolls a popup menu with the 275 * specified scrolling interval. 276 * 277 * @param menu the popup menu 278 * @param interval the scroll interval, in milliseconds 279 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative 280 */ 281 public MenuScroller(JPopupMenu menu, int interval) { 282 this(menu, interval, 0); 283 } 284 285 public MenuScroller(JComponent parentComp, JMenu menu, int interval) { 286 this(menu, interval, 0); 287 parent = parentComp; 288 } 289 290 /** 291 * Constructs a {@code MenuScroller} that scrolls a menu with the 292 * specified scrolling interval, and the specified numbers of items fixed at 293 * the top of the menu. 294 * 295 * @param menu the menu 296 * @param interval the scroll interval, in milliseconds 297 * @param topFixedCount the number of items to fix at the top. May be 0 298 * @throws IllegalArgumentException if scrollCount or interval is 0 or 299 * negative or if topFixedCount is negative 300 */ 301 public MenuScroller(JMenu menu, int interval, int topFixedCount) { 302 this(menu.getPopupMenu(), interval, topFixedCount); 303 } 304 305 public MenuScroller(JComponent parentComp, JMenu menu, int interval, int topFixedCount) { 306 this(menu.getPopupMenu(), interval, topFixedCount); 307 parent = parentComp; 308 } 309 310 /** 311 * Constructs a {@code MenuScroller} that scrolls a popup menu with the 312 * specified scrolling interval, and the specified numbers of items fixed at 313 * the top of the popup menu. 314 * 315 * @param menu the popup menu 316 * @param interval the scroll interval, in milliseconds 317 * @param topFixedCount the number of items to fix at the top. May be 0 318 * @throws IllegalArgumentException if scrollCount or interval is 0 or 319 * negative or if topFixedCount is negative 320 */ 321 public MenuScroller(JPopupMenu menu, int interval, int topFixedCount) { 322 if (interval <= 0) { 323 throw new IllegalArgumentException("interval must be greater than 0"); 324 } 325 if (topFixedCount < 0) { 326 throw new IllegalArgumentException("topFixedCount cannot be negative"); 327 } 328 329 upItem = new MenuScrollItem(MenuIcon.UP, -1); 330 downItem = new MenuScrollItem(MenuIcon.DOWN, +1); 331 setInterval(interval); 332 setTopFixedCount(topFixedCount); 333 334 this.menu = menu; 335 menu.addPopupMenuListener(menuListener); 336 menu.addMouseWheelListener(mouseWheelListener); 337 } 338 339 /** 340 * Returns the scroll interval in milliseconds 341 * 342 * @return the scroll interval in milliseconds 343 */ 344 public int getInterval() { 345 return interval; 346 } 347 348 /** 349 * Sets the scroll interval in milliseconds 350 * 351 * @param interval the scroll interval in milliseconds 352 * @throws IllegalArgumentException if interval is 0 or negative 353 */ 354 public void setInterval(int interval) { 355 if (interval <= 0) { 356 throw new IllegalArgumentException("interval must be greater than 0"); 357 } 358 upItem.setInterval(interval); 359 downItem.setInterval(interval); 360 this.interval = interval; 361 } 362 363 /** 364 * Returns the number of items fixed at the top of the menu or popup menu. 365 * 366 * @return the number of items 367 */ 368 public int getTopFixedCount() { 369 return topFixedCount; 370 } 371 372 /** 373 * Sets the number of items to fix at the top of the menu or popup menu. 374 * 375 * @param topFixedCount the number of items 376 */ 377 public void setTopFixedCount(int topFixedCount) { 378 if (firstIndex <= topFixedCount) { 379 firstIndex = topFixedCount; 380 } else { 381 firstIndex += (topFixedCount - this.topFixedCount); 382 } 383 this.topFixedCount = topFixedCount; 384 } 385 386 /** 387 * Removes this MenuScroller from the associated menu and restores the 388 * default behavior of the menu. 389 */ 390 public void dispose() { 391 if (menu != null) { 392 menu.removePopupMenuListener(menuListener); 393 menu.removeMouseWheelListener(mouseWheelListener); 394 menu.setPreferredSize(null); 395 menu = null; 396 } 397 } 398 399 public void resetMenu() { 400 menuItems = menu.getComponents(); 401 refreshMenu(); 402 } 403 404 /** 405 * Ensures that the {@code dispose} method of this MenuScroller is 406 * called when there are no more refrences to it. 407 * 408 * @exception Throwable if an error occurs. 409 * @see MenuScroller#dispose() 410 */ 411 @Override protected void finalize() throws Throwable { 412 dispose(); 413 super.finalize(); 414 } 415 416 public void setParent(JComponent parent) { 417 this.parent = parent; 418 } 419 420 private void refreshMenu() { 421 if ((menuItems != null) && (menuItems.length > 0)) { 422 423 int allItemsHeight = 0; 424 for (Component item : menuItems) { 425 allItemsHeight += item.getPreferredSize().height; 426 } 427 428 int allowedHeight = getMaxDimensionOnScreen(parent, menu).height - parent.getInsets().top; 429 430 boolean mustScroll = allItemsHeight > allowedHeight; 431 432 if (mustScroll) { 433 firstIndex = Math.max(topFixedCount, firstIndex); 434 int scrollCount = computeScrollCount(firstIndex); 435 firstIndex = Math.min(menuItems.length - scrollCount, firstIndex); 436 437 upItem.setEnabled(firstIndex > topFixedCount); 438 downItem.setEnabled((firstIndex + scrollCount) < menuItems.length); 439 440 menu.removeAll(); 441 for (int i = 0; i < topFixedCount; i++) { 442 menu.add(menuItems[i]); 443 } 444 if (topFixedCount > 0) { 445 menu.addSeparator(); 446 } 447 448 menu.add(upItem); 449 for (int i = firstIndex; i < (scrollCount + firstIndex); i++) { 450 menu.add(menuItems[i]); 451 } 452 menu.add(downItem); 453 454 int preferredWidth = 0; 455 for (Component item : menuItems) { 456 preferredWidth = Math.max(preferredWidth, item.getPreferredSize().width); 457 } 458 menu.setPreferredSize(new Dimension(preferredWidth, menu.getPreferredSize().height)); 459 460 } else if (!Arrays.equals(menu.getComponents(), menuItems)) { 461 // Scroll is not needed but menu is not up to date 462 menu.removeAll(); 463 for (Component item : menuItems) { 464 menu.add(item); 465 } 466 } 467 468 menu.revalidate(); 469 menu.repaint(); 470 } 471 } 472 473 private class MenuScrollListener implements PopupMenuListener { 474 475 @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { 476 setMenuItems(); 477 } 478 479 @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { 480// restoreMenuItems(); 481 setMenuItems(); 482 } 483 484 485 @Override public void popupMenuCanceled(PopupMenuEvent e) { 486// restoreMenuItems(); 487 setMenuItems(); 488 } 489 490 private void setMenuItems() { 491 menuItems = menu.getComponents(); 492 refreshMenu(); 493 } 494 495 private void restoreMenuItems() { 496 menu.removeAll(); 497 for (Component component : menuItems) { 498 menu.add(component); 499 } 500 } 501 } 502 503 private class MenuScrollTimer extends Timer { 504 public MenuScrollTimer(final int increment, int interval) { 505 // software developers hate this one weird trick of a compiler bug 506 // workaround: 507 // http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8006684 508 // 509 // super(interval, e -> { 510 // firstIndex += increment; 511 // refreshMenu(); 512 // }); 513 super(interval, new TimerListener(increment)); 514 } 515 } 516 517 // see the comment in the MenuScrollTimer constructor 518 private class TimerListener implements ActionListener { 519 private int increment; 520 521 public TimerListener(int increment) { 522 this.increment = increment; 523 } 524 525 @Override public void actionPerformed(ActionEvent e) { 526 firstIndex += increment; 527 refreshMenu(); 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 static final int[] XPOINTS = {1, 5, 9}; 563 final int[] yPoints; 564 565 MenuIcon(int... yPoints) { 566 this.yPoints = yPoints; 567 } 568 569 @Override public void paintIcon(Component c, Graphics g, int x, int y) { 570 Dimension size = c.getSize(); 571 Graphics g2 = g.create((size.width / 2) - 5, (size.height / 2) - 5, 10, 10); 572 g2.setColor(Color.GRAY); 573 g2.drawPolygon(XPOINTS, yPoints, 3); 574 if (c.isEnabled()) { 575 g2.setColor(Color.BLACK); 576 g2.fillPolygon(XPOINTS, yPoints, 3); 577 } 578 g2.dispose(); 579 } 580 581 @Override public int getIconWidth() { 582 return 0; 583 } 584 585 @Override public int getIconHeight() { 586 return ARROW_ICON_HEIGHT; 587 } 588 } 589 590 private class MouseScrollListener implements MouseWheelListener { 591 @Override public void mouseWheelMoved(MouseWheelEvent mwe) { 592 firstIndex += mwe.getWheelRotation(); 593 refreshMenu(); 594 mwe.consume(); 595 } 596 } 597}