Merge branch 'scrollmenu' into 'master'

Support scrolling when abilities Popup has too many entries

See merge request core-developers/forge!4567
This commit is contained in:
Michael Kamensky
2021-04-23 04:28:50 +00:00
2 changed files with 638 additions and 0 deletions

View File

@@ -0,0 +1,634 @@
/**
* @(#)MenuScroller.java 1.5.0 04/02/12
* You are free to use and/or modify and/or distribute any or all code posted on the Java Tips Weblog without restriction.
*/
package forge.gui;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.MenuSelectionManager;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
/**
* A class that provides scrolling capabilities to a long menu dropdown or
* popup menu. A number of items can optionally be frozen at the top and/or
* bottom of the menu.
* <P>
* <B>Implementation note:</B> The default number of items to display
* at a time is 15, and the default scrolling interval is 125 milliseconds.
* <P>
*
* @version 1.5.0 04/05/12
* @author Darryl Burke
* @see https://tips4java.wordpress.com/2009/02/01/menu-scroller/
*/
public class MenuScroller {
//private JMenu menu;
private JPopupMenu menu;
private Component[] menuItems;
private MenuScrollItem upItem;
private MenuScrollItem downItem;
private final MenuScrollListener menuListener = new MenuScrollListener();
private final MouseWheelListener mouseWheelListener = new MouseScrollListener();
private int scrollCount;
private int interval;
private int topFixedCount;
private int bottomFixedCount;
private int firstIndex = 0;
private int keepVisibleIndex = -1;
/**
* Registers a menu to be scrolled with the default number of items to
* display at a time and the default scrolling interval.
*
* @param menu the menu
* @return the MenuScroller
*/
public static MenuScroller setScrollerFor(JMenu menu) {
return new MenuScroller(menu);
}
/**
* Registers a popup menu to be scrolled with the default number of items to
* display at a time and the default scrolling interval.
*
* @param menu the popup menu
* @return the MenuScroller
*/
public static MenuScroller setScrollerFor(JPopupMenu menu) {
return new MenuScroller(menu);
}
/**
* Registers a menu to be scrolled with the default number of items to
* display at a time and the specified scrolling interval.
*
* @param menu the menu
* @param scrollCount the number of items to display at a time
* @return the MenuScroller
* @throws IllegalArgumentException if scrollCount is 0 or negative
*/
public static MenuScroller setScrollerFor(JMenu menu, int scrollCount) {
return new MenuScroller(menu, scrollCount);
}
/**
* Registers a popup menu to be scrolled with the default number of items to
* display at a time and the specified scrolling interval.
*
* @param menu the popup menu
* @param scrollCount the number of items to display at a time
* @return the MenuScroller
* @throws IllegalArgumentException if scrollCount is 0 or negative
*/
public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount) {
return new MenuScroller(menu, scrollCount);
}
/**
* Registers a menu to be scrolled, with the specified number of items to
* display at a time and the specified scrolling interval.
*
* @param menu the menu
* @param scrollCount the number of items to be displayed at a time
* @param interval the scroll interval, in milliseconds
* @return the MenuScroller
* @throws IllegalArgumentException if scrollCount or interval is 0 or negative
*/
public static MenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval) {
return new MenuScroller(menu, scrollCount, interval);
}
/**
* Registers a popup menu to be scrolled, with the specified number of items to
* display at a time and the specified scrolling interval.
*
* @param menu the popup menu
* @param scrollCount the number of items to be displayed at a time
* @param interval the scroll interval, in milliseconds
* @return the MenuScroller
* @throws IllegalArgumentException if scrollCount or interval is 0 or negative
*/
public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount, int interval) {
return new MenuScroller(menu, scrollCount, interval);
}
/**
* Registers a menu to be scrolled, with the specified number of items
* to display in the scrolling region, the specified scrolling interval,
* and the specified numbers of items fixed at the top and bottom of the
* menu.
*
* @param menu the menu
* @param scrollCount the number of items to display in the scrolling portion
* @param interval the scroll interval, in milliseconds
* @param topFixedCount the number of items to fix at the top. May be 0.
* @param bottomFixedCount the number of items to fix at the bottom. May be 0
* @throws IllegalArgumentException if scrollCount or interval is 0 or
* negative or if topFixedCount or bottomFixedCount is negative
* @return the MenuScroller
*/
public static MenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval,
int topFixedCount, int bottomFixedCount) {
return new MenuScroller(menu, scrollCount, interval,
topFixedCount, bottomFixedCount);
}
/**
* Registers a popup menu to be scrolled, with the specified number of items
* to display in the scrolling region, the specified scrolling interval,
* and the specified numbers of items fixed at the top and bottom of the
* popup menu.
*
* @param menu the popup menu
* @param scrollCount the number of items to display in the scrolling portion
* @param interval the scroll interval, in milliseconds
* @param topFixedCount the number of items to fix at the top. May be 0
* @param bottomFixedCount the number of items to fix at the bottom. May be 0
* @throws IllegalArgumentException if scrollCount or interval is 0 or
* negative or if topFixedCount or bottomFixedCount is negative
* @return the MenuScroller
*/
public static MenuScroller setScrollerFor(JPopupMenu menu, int scrollCount, int interval,
int topFixedCount, int bottomFixedCount) {
return new MenuScroller(menu, scrollCount, interval,
topFixedCount, bottomFixedCount);
}
/**
* Constructs a <code>MenuScroller</code> that scrolls a menu with the
* default number of items to display at a time, and default scrolling
* interval.
*
* @param menu the menu
*/
public MenuScroller(JMenu menu) {
this(menu, 15);
}
/**
* Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
* default number of items to display at a time, and default scrolling
* interval.
*
* @param menu the popup menu
*/
public MenuScroller(JPopupMenu menu) {
this(menu, 15);
}
/**
* Constructs a <code>MenuScroller</code> that scrolls a menu with the
* specified number of items to display at a time, and default scrolling
* interval.
*
* @param menu the menu
* @param scrollCount the number of items to display at a time
* @throws IllegalArgumentException if scrollCount is 0 or negative
*/
public MenuScroller(JMenu menu, int scrollCount) {
this(menu, scrollCount, 150);
}
/**
* Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
* specified number of items to display at a time, and default scrolling
* interval.
*
* @param menu the popup menu
* @param scrollCount the number of items to display at a time
* @throws IllegalArgumentException if scrollCount is 0 or negative
*/
public MenuScroller(JPopupMenu menu, int scrollCount) {
this(menu, scrollCount, 150);
}
/**
* Constructs a <code>MenuScroller</code> that scrolls a menu with the
* specified number of items to display at a time, and specified scrolling
* interval.
*
* @param menu the menu
* @param scrollCount the number of items to display at a time
* @param interval the scroll interval, in milliseconds
* @throws IllegalArgumentException if scrollCount or interval is 0 or negative
*/
public MenuScroller(JMenu menu, int scrollCount, int interval) {
this(menu, scrollCount, interval, 0, 0);
}
/**
* Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
* specified number of items to display at a time, and specified scrolling
* interval.
*
* @param menu the popup menu
* @param scrollCount the number of items to display at a time
* @param interval the scroll interval, in milliseconds
* @throws IllegalArgumentException if scrollCount or interval is 0 or negative
*/
public MenuScroller(JPopupMenu menu, int scrollCount, int interval) {
this(menu, scrollCount, interval, 0, 0);
}
/**
* Constructs a <code>MenuScroller</code> that scrolls a menu with the
* specified number of items to display in the scrolling region, the
* specified scrolling interval, and the specified numbers of items fixed at
* the top and bottom of the menu.
*
* @param menu the menu
* @param scrollCount the number of items to display in the scrolling portion
* @param interval the scroll interval, in milliseconds
* @param topFixedCount the number of items to fix at the top. May be 0
* @param bottomFixedCount the number of items to fix at the bottom. May be 0
* @throws IllegalArgumentException if scrollCount or interval is 0 or
* negative or if topFixedCount or bottomFixedCount is negative
*/
public MenuScroller(JMenu menu, int scrollCount, int interval,
int topFixedCount, int bottomFixedCount) {
this(menu.getPopupMenu(), scrollCount, interval, topFixedCount, bottomFixedCount);
}
/**
* Constructs a <code>MenuScroller</code> that scrolls a popup menu with the
* specified number of items to display in the scrolling region, the
* specified scrolling interval, and the specified numbers of items fixed at
* the top and bottom of the popup menu.
*
* @param menu the popup menu
* @param scrollCount the number of items to display in the scrolling portion
* @param interval the scroll interval, in milliseconds
* @param topFixedCount the number of items to fix at the top. May be 0
* @param bottomFixedCount the number of items to fix at the bottom. May be 0
* @throws IllegalArgumentException if scrollCount or interval is 0 or
* negative or if topFixedCount or bottomFixedCount is negative
*/
public MenuScroller(JPopupMenu menu, int scrollCount, int interval,
int topFixedCount, int bottomFixedCount) {
if (scrollCount <= 0 || interval <= 0) {
throw new IllegalArgumentException("scrollCount and interval must be greater than 0");
}
if (topFixedCount < 0 || bottomFixedCount < 0) {
throw new IllegalArgumentException("topFixedCount and bottomFixedCount cannot be negative");
}
upItem = new MenuScrollItem(MenuIcon.UP, -1);
downItem = new MenuScrollItem(MenuIcon.DOWN, +1);
setScrollCount(scrollCount);
setInterval(interval);
setTopFixedCount(topFixedCount);
setBottomFixedCount(bottomFixedCount);
this.menu = menu;
menu.addPopupMenuListener(menuListener);
menu.addMouseWheelListener(mouseWheelListener);
}
/**
* Returns the scroll interval in milliseconds
*
* @return the scroll interval in milliseconds
*/
public int getInterval() {
return interval;
}
/**
* Sets the scroll interval in milliseconds
*
* @param interval the scroll interval in milliseconds
* @throws IllegalArgumentException if interval is 0 or negative
*/
public void setInterval(int interval) {
if (interval <= 0) {
throw new IllegalArgumentException("interval must be greater than 0");
}
upItem.setInterval(interval);
downItem.setInterval(interval);
this.interval = interval;
}
/**
* Returns the number of items in the scrolling portion of the menu.
*
* @return the number of items to display at a time
*/
public int getscrollCount() {
return scrollCount;
}
/**
* Sets the number of items in the scrolling portion of the menu.
*
* @param scrollCount the number of items to display at a time
* @throws IllegalArgumentException if scrollCount is 0 or negative
*/
public void setScrollCount(int scrollCount) {
if (scrollCount <= 0) {
throw new IllegalArgumentException("scrollCount must be greater than 0");
}
this.scrollCount = scrollCount;
MenuSelectionManager.defaultManager().clearSelectedPath();
}
/**
* Returns the number of items fixed at the top of the menu or popup menu.
*
* @return the number of items
*/
public int getTopFixedCount() {
return topFixedCount;
}
/**
* Sets the number of items to fix at the top of the menu or popup menu.
*
* @param topFixedCount the number of items
*/
public void setTopFixedCount(int topFixedCount) {
if (firstIndex <= topFixedCount) {
firstIndex = topFixedCount;
} else {
firstIndex += (topFixedCount - this.topFixedCount);
}
this.topFixedCount = topFixedCount;
}
/**
* Returns the number of items fixed at the bottom of the menu or popup menu.
*
* @return the number of items
*/
public int getBottomFixedCount() {
return bottomFixedCount;
}
/**
* Sets the number of items to fix at the bottom of the menu or popup menu.
*
* @param bottomFixedCount the number of items
*/
public void setBottomFixedCount(int bottomFixedCount) {
this.bottomFixedCount = bottomFixedCount;
}
/**
* Scrolls the specified item into view each time the menu is opened. Call this method with
* <code>null</code> to restore the default behavior, which is to show the menu as it last
* appeared.
*
* @param item the item to keep visible
* @see #keepVisible(int)
*/
public void keepVisible(JMenuItem item) {
if (item == null) {
keepVisibleIndex = -1;
} else {
int index = menu.getComponentIndex(item);
keepVisibleIndex = index;
}
}
/**
* Scrolls the item at the specified index into view each time the menu is opened. Call this
* method with <code>-1</code> to restore the default behavior, which is to show the menu as
* it last appeared.
*
* @param index the index of the item to keep visible
* @see #keepVisible(javax.swing.JMenuItem)
*/
public void keepVisible(int index) {
keepVisibleIndex = index;
}
/**
* Removes this MenuScroller from the associated menu and restores the
* default behavior of the menu.
*/
public void dispose() {
if (menu != null) {
menu.removePopupMenuListener(menuListener);
menu.removeMouseWheelListener(mouseWheelListener);
menu = null;
}
}
/**
* Ensures that the <code>dispose</code> method of this MenuScroller is
* called when there are no more references to it.
*
* @exception Throwable if an error occurs.
* @see MenuScroller#dispose()
*/
@Override
public void finalize() throws Throwable {
dispose();
}
private void refreshMenu() {
if (menuItems != null && menuItems.length > 0) {
firstIndex = Math.max(topFixedCount, firstIndex);
firstIndex = Math.min(menuItems.length - bottomFixedCount - scrollCount, firstIndex);
upItem.setEnabled(firstIndex > topFixedCount);
downItem.setEnabled(firstIndex + scrollCount < menuItems.length - bottomFixedCount);
menu.removeAll();
for (int i = 0; i < topFixedCount; i++) {
menu.add(menuItems[i]);
}
if (topFixedCount > 0) {
menu.addSeparator();
}
menu.add(upItem);
for (int i = firstIndex; i < scrollCount + firstIndex; i++) {
menu.add(menuItems[i]);
}
menu.add(downItem);
if (bottomFixedCount > 0) {
menu.addSeparator();
}
for (int i = menuItems.length - bottomFixedCount; i < menuItems.length; i++) {
menu.add(menuItems[i]);
}
int preferredWidth = 0;
for (Component item : menuItems) {
preferredWidth = Math.max(preferredWidth, item.getPreferredSize().width);
}
menu.setPreferredSize(new Dimension(preferredWidth, menu.getPreferredSize().height));
JComponent parent = (JComponent) upItem.getParent();
parent.revalidate();
parent.repaint();
}
}
private class MenuScrollListener implements PopupMenuListener {
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
setMenuItems();
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
restoreMenuItems();
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
restoreMenuItems();
}
private void setMenuItems() {
menuItems = menu.getComponents();
if (keepVisibleIndex >= topFixedCount
&& keepVisibleIndex <= menuItems.length - bottomFixedCount
&& (keepVisibleIndex > firstIndex + scrollCount
|| keepVisibleIndex < firstIndex)) {
firstIndex = Math.min(firstIndex, keepVisibleIndex);
firstIndex = Math.max(firstIndex, keepVisibleIndex - scrollCount + 1);
}
if (menuItems.length > topFixedCount + scrollCount + bottomFixedCount) {
refreshMenu();
}
}
private void restoreMenuItems() {
menu.removeAll();
for (Component component : menuItems) {
menu.add(component);
}
}
}
private class MenuScrollTimer extends Timer {
public MenuScrollTimer(final int increment, int interval) {
super(interval, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
firstIndex += increment;
refreshMenu();
}
});
}
}
private class MenuScrollItem extends JMenuItem
implements ChangeListener {
private MenuScrollTimer timer;
public MenuScrollItem(MenuIcon icon, int increment) {
setIcon(icon);
setDisabledIcon(icon);
timer = new MenuScrollTimer(increment, interval);
addChangeListener(this);
}
public void setInterval(int interval) {
timer.setDelay(interval);
}
@Override
public void stateChanged(ChangeEvent e) {
if (isArmed() && !timer.isRunning()) {
timer.start();
}
if (!isArmed() && timer.isRunning()) {
timer.stop();
}
}
}
private static enum MenuIcon implements Icon {
UP(9, 1, 9),
DOWN(1, 9, 1);
final int[] xPoints = {1, 5, 9};
final int[] yPoints;
MenuIcon(int... yPoints) {
this.yPoints = yPoints;
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
Dimension size = c.getSize();
Graphics g2 = g.create(size.width / 2 - 5, size.height / 2 - 5, 10, 10);
g2.setColor(Color.GRAY);
g2.drawPolygon(xPoints, yPoints, 3);
if (c.isEnabled()) {
g2.setColor(Color.BLACK);
g2.fillPolygon(xPoints, yPoints, 3);
}
g2.dispose();
}
@Override
public int getIconWidth() {
return 0;
}
@Override
public int getIconHeight() {
return 10;
}
}
private class MouseScrollListener implements MouseWheelListener {
public void mouseWheelMoved(MouseWheelEvent mwe){
firstIndex += mwe.getWheelRotation();
refreshMenu();
mwe.consume();
}
}
/**
* Calculates the number for scrollCount such that the menu fills the available
* vertical space from the point (mouse press) to the bottom of the screen.
*
* @param c The component on which the point parameter is based
* @param pt The point at which the top of the menu will appear (in component coordinate space)
* @param item A menuitem of prototypical height off of which the average height is determined
* @param bottomFixedCount Needed to offset the returned scrollCount
* @return the scrollCount
*/
public static int scrollCountForScreen(Component c, Point pt, JMenuItem item, int bottomFixedCount) {
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
Point ptScreen = new Point(pt);
SwingUtilities.convertPointToScreen(ptScreen, c);
int height = screenSize.height - ptScreen.y;
int miHeight = item.getPreferredSize().height;
int scrollCount = (height / miHeight) - bottomFixedCount - 2; // 2 just takes the menu up a bit from the bottom which looks nicer
return scrollCount;
}
}

View File

@@ -84,6 +84,7 @@ import forge.gui.GuiBase;
import forge.gui.GuiChoose;
import forge.gui.GuiDialog;
import forge.gui.GuiUtils;
import forge.gui.MenuScroller;
import forge.gui.SOverlayUtils;
import forge.gui.framework.DragCell;
import forge.gui.framework.EDocID;
@@ -906,6 +907,9 @@ public final class CMatchUI
//show menu if mouse was trigger for ability
final JPopupMenu menu = new JPopupMenu(Localizer.getInstance().getMessage("lblAbilities"));
//add scroll area when too big
// TODO: do we need a user setting for the scrollCount?
MenuScroller.setScrollerFor(menu, 8, 125, 3, 1);
boolean enabled;
int firstEnabled = -1;