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