001/** 002 * MenuScroller.java 1.5.0 04/02/12 003 * License: use / modify without restrictions (see https://tips4java.wordpress.com/about/) 004 * Heavily modified for JOSM needs => drop unused features and replace static scrollcount approach by dynamic behaviour 005 */ 006package org.openstreetmap.josm.gui; 007 008import java.awt.Color; 009import java.awt.Component; 010import java.awt.Dimension; 011import java.awt.Graphics; 012import java.awt.event.ActionEvent; 013import java.awt.event.ActionListener; 014import java.awt.event.MouseWheelEvent; 015import java.awt.event.MouseWheelListener; 016import java.util.Arrays; 017 018import javax.swing.Icon; 019import javax.swing.JMenu; 020import javax.swing.JMenuItem; 021import javax.swing.JPopupMenu; 022import javax.swing.JSeparator; 023import javax.swing.Timer; 024import javax.swing.event.ChangeEvent; 025import javax.swing.event.ChangeListener; 026import javax.swing.event.PopupMenuEvent; 027import javax.swing.event.PopupMenuListener; 028 029import org.openstreetmap.josm.gui.util.WindowGeometry; 030import org.openstreetmap.josm.tools.Logging; 031 032/** 033 * A class that provides scrolling capabilities to a long menu dropdown or 034 * popup menu. A number of items can optionally be frozen at the top of the menu. 035 * <p> 036 * <b>Implementation note:</B> The default scrolling interval is 150 milliseconds. 037 * <p> 038 * @author Darryl, https://tips4java.wordpress.com/2009/02/01/menu-scroller/ 039 * @since 4593 040 */ 041public class MenuScroller { 042 043 private JPopupMenu menu; 044 private Component[] menuItems; 045 private MenuScrollItem upItem; 046 private MenuScrollItem downItem; 047 private final MenuScrollListener menuListener = new MenuScrollListener(); 048 private final MouseWheelListener mouseWheelListener = new MouseScrollListener(); 049 private int topFixedCount; 050 private int firstIndex; 051 052 private static final int ARROW_ICON_HEIGHT = 10; 053 054 private int computeScrollCount(int startIndex) { 055 int result = 15; 056 if (menu != null) { 057 // Compute max height of current screen 058 int maxHeight = WindowGeometry.getMaxDimensionOnScreen(menu).height - MainApplication.getMainFrame().getInsets().top; 059 060 // Remove top fixed part height 061 if (topFixedCount > 0) { 062 for (int i = 0; i < topFixedCount; i++) { 063 maxHeight -= menuItems[i].getPreferredSize().height; 064 } 065 maxHeight -= new JSeparator().getPreferredSize().height; 066 } 067 068 // Remove height of our two arrow items + insets 069 maxHeight -= menu.getInsets().top; 070 maxHeight -= upItem.getPreferredSize().height; 071 maxHeight -= downItem.getPreferredSize().height; 072 maxHeight -= menu.getInsets().bottom; 073 074 // Compute scroll count 075 result = 0; 076 int height = 0; 077 for (int i = startIndex; i < menuItems.length && height <= maxHeight; i++, result++) { 078 height += menuItems[i].getPreferredSize().height; 079 } 080 081 if (height > maxHeight) { 082 // Remove extra item from count 083 result--; 084 } else { 085 // Increase scroll count to take into account upper items that will be displayed 086 // after firstIndex is updated 087 for (int i = startIndex-1; i >= 0 && height <= maxHeight; i--, result++) { 088 height += menuItems[i].getPreferredSize().height; 089 } 090 if (height > maxHeight) { 091 result--; 092 } 093 } 094 } 095 return result; 096 } 097 098 /** 099 * Registers a menu to be scrolled with the default scrolling interval. 100 * 101 * @param menu the menu 102 * @return the MenuScroller 103 */ 104 public static MenuScroller setScrollerFor(JMenu menu) { 105 return new MenuScroller(menu); 106 } 107 108 /** 109 * Registers a popup menu to be scrolled with the default scrolling interval. 110 * 111 * @param menu the popup menu 112 * @return the MenuScroller 113 */ 114 public static MenuScroller setScrollerFor(JPopupMenu menu) { 115 return new MenuScroller(menu); 116 } 117 118 /** 119 * Registers a menu to be scrolled, with the specified scrolling interval. 120 * 121 * @param menu the menu 122 * @param interval the scroll interval, in milliseconds 123 * @return the MenuScroller 124 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative 125 * @since 7463 126 */ 127 public static MenuScroller setScrollerFor(JMenu menu, int interval) { 128 return new MenuScroller(menu, interval); 129 } 130 131 /** 132 * Registers a popup menu to be scrolled, with the specified scrolling interval. 133 * 134 * @param menu the popup menu 135 * @param interval the scroll interval, in milliseconds 136 * @return the MenuScroller 137 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative 138 * @since 7463 139 */ 140 public static MenuScroller setScrollerFor(JPopupMenu menu, int interval) { 141 return new MenuScroller(menu, interval); 142 } 143 144 /** 145 * Registers a menu to be scrolled, with the specified scrolling interval, 146 * and the specified numbers of items fixed at the top of the menu. 147 * 148 * @param menu the menu 149 * @param interval the scroll interval, in milliseconds 150 * @param topFixedCount the number of items to fix at the top. May be 0. 151 * @return the MenuScroller 152 * @throws IllegalArgumentException if scrollCount or interval is 0 or 153 * negative or if topFixedCount is negative 154 * @since 7463 155 */ 156 public static MenuScroller setScrollerFor(JMenu menu, int interval, int topFixedCount) { 157 return new MenuScroller(menu, interval, topFixedCount); 158 } 159 160 /** 161 * Registers a popup menu to be scrolled, with the specified scrolling interval, 162 * and the specified numbers of items fixed at the top of the popup menu. 163 * 164 * @param menu the popup menu 165 * @param interval the scroll interval, in milliseconds 166 * @param topFixedCount the number of items to fix at the top. May be 0 167 * @return the MenuScroller 168 * @throws IllegalArgumentException if scrollCount or interval is 0 or 169 * negative or if topFixedCount is negative 170 * @since 7463 171 */ 172 public static MenuScroller setScrollerFor(JPopupMenu menu, int interval, int topFixedCount) { 173 return new MenuScroller(menu, interval, topFixedCount); 174 } 175 176 /** 177 * Constructs a <code>MenuScroller</code> that scrolls a menu with the 178 * default scrolling interval. 179 * 180 * @param menu the menu 181 * @throws IllegalArgumentException if scrollCount is 0 or negative 182 */ 183 public MenuScroller(JMenu menu) { 184 this(menu, 150); 185 } 186 187 /** 188 * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the 189 * default scrolling interval. 190 * 191 * @param menu the popup menu 192 * @throws IllegalArgumentException if scrollCount is 0 or negative 193 */ 194 public MenuScroller(JPopupMenu menu) { 195 this(menu, 150); 196 } 197 198 /** 199 * Constructs a <code>MenuScroller</code> that scrolls a menu with the 200 * specified scrolling interval. 201 * 202 * @param menu the menu 203 * @param interval the scroll interval, in milliseconds 204 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative 205 * @since 7463 206 */ 207 public MenuScroller(JMenu menu, int interval) { 208 this(menu, interval, 0); 209 } 210 211 /** 212 * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the 213 * specified scrolling interval. 214 * 215 * @param menu the popup menu 216 * @param interval the scroll interval, in milliseconds 217 * @throws IllegalArgumentException if scrollCount or interval is 0 or negative 218 * @since 7463 219 */ 220 public MenuScroller(JPopupMenu menu, int interval) { 221 this(menu, interval, 0); 222 } 223 224 /** 225 * Constructs a <code>MenuScroller</code> that scrolls a menu with the 226 * specified scrolling interval, and the specified numbers of items fixed at 227 * the top of the menu. 228 * 229 * @param menu the menu 230 * @param interval the scroll interval, in milliseconds 231 * @param topFixedCount the number of items to fix at the top. May be 0 232 * @throws IllegalArgumentException if scrollCount or interval is 0 or 233 * negative or if topFixedCount is negative 234 * @since 7463 235 */ 236 public MenuScroller(JMenu menu, int interval, int topFixedCount) { 237 this(menu.getPopupMenu(), interval, topFixedCount); 238 } 239 240 /** 241 * Constructs a <code>MenuScroller</code> that scrolls a popup menu with the 242 * specified scrolling interval, and the specified numbers of items fixed at 243 * the top of the popup menu. 244 * 245 * @param menu the popup menu 246 * @param interval the scroll interval, in milliseconds 247 * @param topFixedCount the number of items to fix at the top. May be 0 248 * @throws IllegalArgumentException if scrollCount or interval is 0 or 249 * negative or if topFixedCount is negative 250 * @since 7463 251 */ 252 public MenuScroller(JPopupMenu menu, int interval, int topFixedCount) { 253 if (interval <= 0) { 254 throw new IllegalArgumentException("interval must be greater than 0"); 255 } 256 if (topFixedCount < 0) { 257 throw new IllegalArgumentException("topFixedCount cannot be negative"); 258 } 259 260 upItem = new MenuScrollItem(MenuIcon.UP, -1, interval); 261 downItem = new MenuScrollItem(MenuIcon.DOWN, +1, interval); 262 setTopFixedCount(topFixedCount); 263 264 this.menu = menu; 265 menu.addPopupMenuListener(menuListener); 266 menu.addMouseWheelListener(mouseWheelListener); 267 } 268 269 /** 270 * Returns the number of items fixed at the top of the menu or popup menu. 271 * 272 * @return the number of items 273 */ 274 public int getTopFixedCount() { 275 return topFixedCount; 276 } 277 278 /** 279 * Sets the number of items to fix at the top of the menu or popup menu. 280 * 281 * @param topFixedCount the number of items 282 */ 283 public void setTopFixedCount(int topFixedCount) { 284 if (firstIndex <= topFixedCount) { 285 firstIndex = topFixedCount; 286 } else { 287 firstIndex += (topFixedCount - this.topFixedCount); 288 } 289 this.topFixedCount = topFixedCount; 290 } 291 292 /** 293 * Removes this MenuScroller from the associated menu and restores the 294 * default behavior of the menu. 295 */ 296 public void dispose() { 297 if (menu != null) { 298 menu.removePopupMenuListener(menuListener); 299 menu.removeMouseWheelListener(mouseWheelListener); 300 menu.setPreferredSize(null); 301 menu = null; 302 } 303 } 304 305 private void refreshMenu() { 306 if (menuItems != null && menuItems.length > 0) { 307 308 int allItemsHeight = 0; 309 for (Component item : menuItems) { 310 allItemsHeight += item.getPreferredSize().height; 311 } 312 313 int allowedHeight = WindowGeometry.getMaxDimensionOnScreen(menu).height - MainApplication.getMainFrame().getInsets().top; 314 315 boolean mustSCroll = allItemsHeight > allowedHeight; 316 317 if (mustSCroll) { 318 firstIndex = Math.min(menuItems.length-1, Math.max(topFixedCount, firstIndex)); 319 int scrollCount = computeScrollCount(firstIndex); 320 firstIndex = Math.min(menuItems.length - scrollCount, firstIndex); 321 322 upItem.setEnabled(firstIndex > topFixedCount); 323 downItem.setEnabled(firstIndex + scrollCount < menuItems.length); 324 325 menu.removeAll(); 326 for (int i = 0; i < topFixedCount; i++) { 327 menu.add(menuItems[i]); 328 } 329 if (topFixedCount > 0) { 330 menu.addSeparator(); 331 } 332 333 menu.add(upItem); 334 for (int i = firstIndex; i < scrollCount + firstIndex; i++) { 335 menu.add(menuItems[i]); 336 } 337 menu.add(downItem); 338 339 int preferredWidth = 0; 340 for (Component item : menuItems) { 341 preferredWidth = Math.max(preferredWidth, item.getPreferredSize().width); 342 } 343 menu.setPreferredSize(new Dimension(preferredWidth, menu.getPreferredSize().height)); 344 345 } else if (!Arrays.equals(menu.getComponents(), menuItems)) { 346 // Scroll is not needed but menu is not up to date 347 menu.removeAll(); 348 for (Component item : menuItems) { 349 menu.add(item); 350 } 351 } 352 353 menu.revalidate(); 354 menu.repaint(); 355 } 356 } 357 358 private class MenuScrollListener implements PopupMenuListener { 359 360 @Override 361 public void popupMenuWillBecomeVisible(PopupMenuEvent e) { 362 setMenuItems(); 363 } 364 365 @Override 366 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { 367 restoreMenuItems(); 368 } 369 370 @Override 371 public void popupMenuCanceled(PopupMenuEvent e) { 372 restoreMenuItems(); 373 } 374 375 private void setMenuItems() { 376 menuItems = menu.getComponents(); 377 refreshMenu(); 378 } 379 380 private void restoreMenuItems() { 381 menu.removeAll(); 382 for (Component component : menuItems) { 383 menu.add(component); 384 } 385 } 386 } 387 388 private class MenuScrollTimer extends Timer { 389 390 MenuScrollTimer(final int increment, int interval) { 391 super(interval, new ActionListener() { 392 393 @Override 394 public void actionPerformed(ActionEvent e) { 395 firstIndex += increment; 396 refreshMenu(); 397 } 398 }); 399 } 400 } 401 402 private class MenuScrollItem extends JMenuItem 403 implements ChangeListener { 404 405 private final MenuScrollTimer timer; 406 407 MenuScrollItem(MenuIcon icon, int increment, int interval) { 408 setIcon(icon); 409 setDisabledIcon(icon); 410 timer = new MenuScrollTimer(increment, interval); 411 addChangeListener(this); 412 } 413 414 @Override 415 public void stateChanged(ChangeEvent e) { 416 if (isArmed() && !timer.isRunning()) { 417 timer.start(); 418 } 419 if (!isArmed() && timer.isRunning()) { 420 timer.stop(); 421 } 422 } 423 } 424 425 private enum MenuIcon implements Icon { 426 427 UP(9, 1, 9), 428 DOWN(1, 9, 1); 429 private static final int[] XPOINTS = {1, 5, 9}; 430 private final int[] yPoints; 431 432 MenuIcon(int... yPoints) { 433 this.yPoints = yPoints; 434 } 435 436 @Override 437 public void paintIcon(Component c, Graphics g, int x, int y) { 438 Dimension size = c.getSize(); 439 Graphics g2 = g.create(size.width / 2 - 5, size.height / 2 - 5, 10, 10); 440 g2.setColor(Color.GRAY); 441 g2.drawPolygon(XPOINTS, yPoints, 3); 442 if (c.isEnabled()) { 443 g2.setColor(Color.BLACK); 444 g2.fillPolygon(XPOINTS, yPoints, 3); 445 } 446 g2.dispose(); 447 } 448 449 @Override 450 public int getIconWidth() { 451 return 0; 452 } 453 454 @Override 455 public int getIconHeight() { 456 return ARROW_ICON_HEIGHT; 457 } 458 } 459 460 private class MouseScrollListener implements MouseWheelListener { 461 @Override 462 public void mouseWheelMoved(MouseWheelEvent mwe) { 463 firstIndex += mwe.getWheelRotation(); 464 refreshMenu(); 465 if (Logging.isDebugEnabled()) { 466 Logging.debug("{0} consuming event {1}", getClass().getName(), mwe); 467 } 468 mwe.consume(); 469 } 470 } 471}