diff --git a/forge-gui-desktop/src/main/java/forge/gui/MenuScroller.java b/forge-gui-desktop/src/main/java/forge/gui/MenuScroller.java new file mode 100644 index 00000000000..b20fb3261f3 --- /dev/null +++ b/forge-gui-desktop/src/main/java/forge/gui/MenuScroller.java @@ -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. + *
+ * Implementation note: The default number of items to display + * at a time is 15, and the default scrolling interval is 125 milliseconds. + *
+ *
+ * @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 MenuScroller 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 MenuScroller 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 MenuScroller 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 MenuScroller 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 MenuScroller 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 MenuScroller 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 MenuScroller 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 MenuScroller 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
+ * null 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 -1 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 dispose 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;
+ }
+}
diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java
index 01887035ddb..6af24207671 100644
--- a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java
+++ b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java
@@ -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;