Adding vertical scroll to a JPopupMenu?
Asked Answered
I

9

18

I would like to add a way to scroll through menu items in a JPopupMenu, much like scrolling through a list of items in a JComboBox.

Let's say I have 10 menu items. I would like to display only 5 menu items at a time, and I would use a vertical scroll button at the bottom or top of the JPopupMenu to show the menu items that are not listed and hide the ones that I just saw.

Is it possible? I am using JIDE Software's JideSplitButton, which displays a JPopupMenu when clicked. I am trying to keep the look and feel of the command bar on which I placed the JideSplitButton, so I don't want to replace it with a JComboBox unless I really have to.

Illyes answered 15/2, 2012 at 5:34 Comment(0)
H
5

May be this http://www.javabeginner.com/java-swing/java-scrollable-popup-menu

Hagiocracy answered 15/2, 2012 at 8:45 Comment(2)
link is now deadCeladon
can't edit..the link via wayback is: web.archive.org/web/20150416013923/http://www.javabeginner.com/…Squireen
R
30

Here is a version I created using a scrollbar. It is just a simple example, so adapt as you see fit:

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import javax.swing.JPopupMenu;
import javax.swing.JScrollBar;

public class JScrollPopupMenu extends JPopupMenu {
    protected int maximumVisibleRows = 10;

    public JScrollPopupMenu() {
        this(null);
    }


    public JScrollPopupMenu(String label) {
        super(label);
        setLayout(new ScrollPopupMenuLayout());

        super.add(getScrollBar());
        addMouseWheelListener(new MouseWheelListener() {
            @Override public void mouseWheelMoved(MouseWheelEvent event) {
                JScrollBar scrollBar = getScrollBar();
                int amount = (event.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL)
                             ? event.getUnitsToScroll() * scrollBar.getUnitIncrement()
                             : (event.getWheelRotation() < 0 ? -1 : 1) * scrollBar.getBlockIncrement();

                scrollBar.setValue(scrollBar.getValue() + amount);
                event.consume();
            }
        });
    }

    private JScrollBar popupScrollBar;
    protected JScrollBar getScrollBar() {
        if(popupScrollBar == null) {
            popupScrollBar = new JScrollBar(JScrollBar.VERTICAL);
            popupScrollBar.addAdjustmentListener(new AdjustmentListener() {
                @Override public void adjustmentValueChanged(AdjustmentEvent e) {
                    doLayout();
                    repaint();
                }
            });

            popupScrollBar.setVisible(false);
        }

        return popupScrollBar;
    }

    public int getMaximumVisibleRows() {
        return maximumVisibleRows;
    }

    public void setMaximumVisibleRows(int maximumVisibleRows) {
        this.maximumVisibleRows = maximumVisibleRows;
    }

    public void paintChildren(Graphics g){
        Insets insets = getInsets();
        g.clipRect(insets.left, insets.top, getWidth(), getHeight() - insets.top - insets.bottom);
        super.paintChildren(g);
    }

    protected void addImpl(Component comp, Object constraints, int index) {
        super.addImpl(comp, constraints, index);

        if(maximumVisibleRows < getComponentCount()-1) {
            getScrollBar().setVisible(true);
        }
    }

    public void remove(int index) {
        // can't remove the scrollbar
        ++index;

        super.remove(index);

        if(maximumVisibleRows >= getComponentCount()-1) {
            getScrollBar().setVisible(false);
        }
    }

    public void show(Component invoker, int x, int y){
        JScrollBar scrollBar = getScrollBar();
        if(scrollBar.isVisible()){
            int extent = 0;
            int max = 0;
            int i = 0;
            int unit = -1;
            int width = 0;
            for(Component comp : getComponents()) {
                if(!(comp instanceof JScrollBar)) {
                    Dimension preferredSize = comp.getPreferredSize();
                    width = Math.max(width, preferredSize.width);
                    if(unit < 0){
                        unit = preferredSize.height;
                    }
                    if(i++ < maximumVisibleRows) {
                        extent += preferredSize.height;
                    }
                    max += preferredSize.height;
                }
            }

            Insets insets = getInsets();
            int widthMargin = insets.left + insets.right;
            int heightMargin = insets.top + insets.bottom;
            scrollBar.setUnitIncrement(unit);
            scrollBar.setBlockIncrement(extent);
            scrollBar.setValues(0, heightMargin + extent, 0, heightMargin + max);

            width += scrollBar.getPreferredSize().width + widthMargin;
            int height = heightMargin + extent;

            setPopupSize(new Dimension(width, height));
        }

        super.show(invoker, x, y);
    }

    protected static class ScrollPopupMenuLayout implements LayoutManager{
        @Override public void addLayoutComponent(String name, Component comp) {}
        @Override public void removeLayoutComponent(Component comp) {}

        @Override public Dimension preferredLayoutSize(Container parent) {
            int visibleAmount = Integer.MAX_VALUE;
            Dimension dim = new Dimension();
            for(Component comp :parent.getComponents()){
                if(comp.isVisible()) {
                    if(comp instanceof JScrollBar){
                        JScrollBar scrollBar = (JScrollBar) comp;
                        visibleAmount = scrollBar.getVisibleAmount();
                    }
                    else {
                        Dimension pref = comp.getPreferredSize();
                        dim.width = Math.max(dim.width, pref.width);
                        dim.height += pref.height;
                    }
                }
            }

            Insets insets = parent.getInsets();
            dim.height = Math.min(dim.height + insets.top + insets.bottom, visibleAmount);

            return dim;
        }

        @Override public Dimension minimumLayoutSize(Container parent) {
            int visibleAmount = Integer.MAX_VALUE;
            Dimension dim = new Dimension();
            for(Component comp : parent.getComponents()) {
                if(comp.isVisible()){
                    if(comp instanceof JScrollBar) {
                        JScrollBar scrollBar = (JScrollBar) comp;
                        visibleAmount = scrollBar.getVisibleAmount();
                    }
                    else {
                        Dimension min = comp.getMinimumSize();
                        dim.width = Math.max(dim.width, min.width);
                        dim.height += min.height;
                    }
                }
            }

            Insets insets = parent.getInsets();
            dim.height = Math.min(dim.height + insets.top + insets.bottom, visibleAmount);

            return dim;
        }

        @Override public void layoutContainer(Container parent) {
            Insets insets = parent.getInsets();

            int width = parent.getWidth() - insets.left - insets.right;
            int height = parent.getHeight() - insets.top - insets.bottom;

            int x = insets.left;
            int y = insets.top;
            int position = 0;

            for(Component comp : parent.getComponents()) {
                if((comp instanceof JScrollBar) && comp.isVisible()) {
                    JScrollBar scrollBar = (JScrollBar) comp;
                    Dimension dim = scrollBar.getPreferredSize();
                    scrollBar.setBounds(x + width-dim.width, y, dim.width, height);
                    width -= dim.width;
                    position = scrollBar.getValue();
                }
            }

            y -= position;
            for(Component comp : parent.getComponents()) {
                if(!(comp instanceof JScrollBar) && comp.isVisible()) {
                    Dimension pref = comp.getPreferredSize();
                    comp.setBounds(x, y, width, pref.height);
                    y += pref.height;
                }
            }
        }
    }
}
Raskin answered 5/1, 2013 at 0:5 Comment(4)
@Raskin shouldn't you also reimplement other methods that rely on indexes of the components? Like insert(Component component, int index) and insert(Action a, int index)?Ty
@andrybak, Yes you should. I just wanted to create a simple, but fully functional example. For completeness, I'm sure there is more that should be done.Raskin
This is really nice - one thing I cant figure though is how to make the displayed menu items scroll if using the cursor keys within the menu to navigate up and down, such that the currently selected item is always visible?Annadiane
@Annadiane See my answer below https://mcmap.net/q/649058/-adding-vertical-scroll-to-a-jpopupmenuPontic
R
16

In addition to the JScrollPopupMenu above, I also needed a a scroll bar in a sub menu (aka a "Pull Right Menu.") This seems to be a more common case. So I adapted a JMenu to use the JScrollPopupMenu called JScrollMenu:

import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.MenuElement;
import javax.swing.UIManager;
import javax.swing.plaf.MenuItemUI;
import javax.swing.plaf.PopupMenuUI;
import java.awt.Component;
import java.awt.ComponentOrientation;



public class JScrollMenu extends JMenu {
    // Covers the one in the JMenu because the method that creates it in JMenu is private
    /** The popup menu portion of the menu.*/
    private JPopupMenu popupMenu;


    /**
     * Constructs a new <code>JMenu</code> with no text.
     */
    public JScrollMenu() {
        this("");
    }

    /**
     * Constructs a new <code>JMenu</code> with the supplied string as its text.
     *
     * @param s the text for the menu label
     */
    public JScrollMenu(String s) {
        super(s);
    }

    /**
     * Constructs a menu whose properties are taken from the <code>Action</code> supplied.
     *
     * @param a an <code>Action</code>
     */
    public JScrollMenu(Action a) {
        this();
        setAction(a);
    }


    /**
     * Lazily creates the popup menu. This method will create the popup using the <code>JScrollPopupMenu</code> class. 
     */
    protected void ensurePopupMenuCreated() {
        if(popupMenu == null) {
            this.popupMenu = new JScrollPopupMenu();
            popupMenu.setInvoker(this);
            popupListener = createWinListener(popupMenu);
        }
    }

//////////////////////////////
//// All of these methods are necessary because ensurePopupMenuCreated() is private in JMenu
//////////////////////////////
    @Override
    public void updateUI() {
        setUI((MenuItemUI) UIManager.getUI(this));

        if(popupMenu != null) {
            popupMenu.setUI((PopupMenuUI) UIManager.getUI(popupMenu));
        }
    }


    @Override
    public boolean isPopupMenuVisible() {
        ensurePopupMenuCreated();
        return popupMenu.isVisible();
    }


    @Override
    public void setMenuLocation(int x, int y) {
        super.setMenuLocation(x, y);
        if(popupMenu != null) {
            popupMenu.setLocation(x, y);
        }
    }

    @Override
    public JMenuItem add(JMenuItem menuItem) {
        ensurePopupMenuCreated();
        return popupMenu.add(menuItem);
    }

    @Override
    public Component add(Component c) {
        ensurePopupMenuCreated();
        popupMenu.add(c);
        return c;
    }

    @Override
    public Component add(Component c, int index) {
        ensurePopupMenuCreated();
        popupMenu.add(c, index);
        return c;
    }


    @Override
    public void addSeparator() {
        ensurePopupMenuCreated();
        popupMenu.addSeparator();
    }

    @Override
    public void insert(String s, int pos) {
        if(pos < 0) {
            throw new IllegalArgumentException("index less than zero.");
        }

        ensurePopupMenuCreated();
        popupMenu.insert(new JMenuItem(s), pos);
    }

    @Override
    public JMenuItem insert(JMenuItem mi, int pos) {
        if(pos < 0) {
            throw new IllegalArgumentException("index less than zero.");
        }
        ensurePopupMenuCreated();
        popupMenu.insert(mi, pos);
        return mi;
    }

    @Override
    public JMenuItem insert(Action a, int pos) {
        if(pos < 0) {
            throw new IllegalArgumentException("index less than zero.");
        }

        ensurePopupMenuCreated();
        JMenuItem mi = new JMenuItem(a);
        mi.setHorizontalTextPosition(JButton.TRAILING);
        mi.setVerticalTextPosition(JButton.CENTER);
        popupMenu.insert(mi, pos);
        return mi;
    }

    @Override
    public void insertSeparator(int index) {
        if(index < 0) {
            throw new IllegalArgumentException("index less than zero.");
        }

        ensurePopupMenuCreated();
        popupMenu.insert(new JPopupMenu.Separator(), index);
    }


    @Override
    public void remove(JMenuItem item) {
        if(popupMenu != null){
            popupMenu.remove(item);
        }
    }

    @Override
    public void remove(int pos) {
        if(pos < 0) {
            throw new IllegalArgumentException("index less than zero.");
        }
        if(pos > getItemCount()) {
            throw new IllegalArgumentException("index greater than the number of items.");
        }
        if(popupMenu != null){
            popupMenu.remove(pos);
        }
    }

    @Override
    public void remove(Component c) {
        if(popupMenu != null){
            popupMenu.remove(c);
        }
    }

    @Override
    public void removeAll() {
        if(popupMenu != null){
            popupMenu.removeAll();
        }
    }

    @Override
    public int getMenuComponentCount() {
        return (popupMenu == null) ? 0 : popupMenu.getComponentCount();
    }

    @Override
    public Component getMenuComponent(int n) {
        return (popupMenu == null) ? null : popupMenu.getComponent(n);
    }

    @Override
    public Component[] getMenuComponents() {
        return (popupMenu == null) ? new Component[0] : popupMenu.getComponents();
    }

    @Override
    public JPopupMenu getPopupMenu() {
        ensurePopupMenuCreated();
        return popupMenu;
    }

    @Override
    public MenuElement[] getSubElements() {
        return popupMenu == null ? new MenuElement[0] : new MenuElement[]{popupMenu};
    }


    @Override
    public void applyComponentOrientation(ComponentOrientation o) {
        super.applyComponentOrientation(o);

        if(popupMenu != null) {
            int ncomponents = getMenuComponentCount();
            for(int i = 0; i < ncomponents; ++i) {
                getMenuComponent(i).applyComponentOrientation(o);
            }
            popupMenu.setComponentOrientation(o);
        }
    }

    @Override
    public void setComponentOrientation(ComponentOrientation o) {
        super.setComponentOrientation(o);
        if(popupMenu != null) {
            popupMenu.setComponentOrientation(o);
        }
    }
}
Raskin answered 27/2, 2013 at 19:52 Comment(2)
This is a nice solution :) ... I am trying to figure out how to force scrolling to a specific item in the sub menu using this solution, like using ensureIndexIsVisible(index) in a JList ?Astilbe
public void ensureIndexIsVisible(int index) { Rectangle rect = getMenuComponent(index).getBounds(); ((JScrollPopupMenu)popupMenu).getScrollBar().scrollRectToVisible(rect); }Raskin
A
7

Here's another one I found very useful: https://tips4java.wordpress.com/2009/02/01/menu-scroller/

It can be called on JMenu or JPopupMenu like this:

MenuScroller.setScrollerFor(menuInstance, 8, 125, 3, 1);

menu scroller

Aluminate answered 21/3, 2017 at 12:57 Comment(0)
H
5

May be this http://www.javabeginner.com/java-swing/java-scrollable-popup-menu

Hagiocracy answered 15/2, 2012 at 8:45 Comment(2)
link is now deadCeladon
can't edit..the link via wayback is: web.archive.org/web/20150416013923/http://www.javabeginner.com/…Squireen
A
4

Basically you can add any JComponents to the JPopupMenu, you can add JScrollpane to the JPopup by nesting JPanel / JList with another JComponents,

Notice but there is rule that swing GUI doesn't allowing two lightweight popup window in same time, best example is common Bug in Swing about JComboBox in the JPopup

you have look at JWindow, create once time and re_use that for another Action, nothing best around as to check how popup JWindow really works for JCalendar by Kai Toedter

Alsace answered 15/2, 2012 at 8:45 Comment(0)
M
0

Alternately you may want to consider JidePopupMenu: Scrollable JPopupMenu

Malorie answered 13/6, 2013 at 11:43 Comment(0)
R
0

As I needed popup menu with Scrollbar, I just reused popup menu from JComboBox. The trick was to put JComboBox in a JViewport, so that only arrow button was visible. You may make it just one pixel small or even lesser and use event from JideSplitButton to open popup.

You may find code on github.

Rael answered 10/7, 2016 at 18:24 Comment(0)
P
0

Adding to the JScrollPopupMenu answer above (I can't edit it).

In order to have it scroll on arrow navigation etc., I added this:

@Override public void scrollRectToVisible(Rectangle aRect) {
    final Insets insets = getInsets();
    final int scrollY = popupScrollBar.getValue();
    int y = aRect.y;
    if (y - insets.top < 0)
        popupScrollBar.setValue(scrollY + y - insets.top);
    else {
        y += aRect.height;
        final int bottom = getHeight() - insets.bottom;
        if (y > bottom)
            popupScrollBar.setValue(scrollY + y - bottom);
    }
}

Which then be called like so:

popupMenu.scrollRectToVisible(menuItem.getBounds());
Pontic answered 22/9, 2022 at 6:8 Comment(1)
Related for arming menu items: https://mcmap.net/q/673208/-jmenuitem-setselected-does-not-alter-appearance-of-selected-itemPontic
F
0

For precise scroll (trackpad, copy from com.formdev.flatlaf.ui.FlatScrollPaneUI.mouseWheelMovedSmooth):

public JScrollPopupMenu(String label) {
    super(label);
    setLayout(new ScrollPopupMenuLayout());

    super.add(getScrollBar());
    addMouseWheelListener(e -> {
        if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL &&
                e.getPreciseWheelRotation() != 0 &&
                e.getPreciseWheelRotation() != e.getWheelRotation()) {
            mouseWheelMovedSmooth(e);
        } else {
            JScrollBar scrollBar = getScrollBar();
            int amount = (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL)
                    ? e.getUnitsToScroll() * scrollBar.getUnitIncrement()
                    : (e.getWheelRotation() < 0 ? -1 : 1) * scrollBar.getBlockIncrement();

            scrollBar.setValue(scrollBar.getValue() + amount);
            e.consume();
        }
    });
}

...

// com.formdev.flatlaf.ui.FlatScrollPaneUI.mouseWheelMovedSmooth
private void mouseWheelMovedSmooth(MouseWheelEvent e) {
    // find scrollbar to scroll
    JScrollBar scrollbar = popupScrollBar;

    // consume event
    e.consume();

    // get precise wheel rotation
    double rotation = e.getPreciseWheelRotation();

    // get unit increment
    int orientation = scrollbar.getOrientation();
    int direction = rotation < 0 ? -1 : 1;
    int unitIncrement = scrollbar.getUnitIncrement(direction);

    // get viewport width/height (the visible width/height)
    int viewportWH = (orientation == SwingConstants.VERTICAL)
            ? this.getHeight()
            : this.getWidth();

    // limit scroll increment to viewport width/height
    // - if scroll amount is set to a large value in OS settings
    // - for large unit increments in small viewports (e.g. horizontal scrolling in file chooser)
    int scrollIncrement = Math.min(unitIncrement * e.getScrollAmount(), viewportWH);

    // compute relative delta
    double delta = rotation * scrollIncrement;
    int idelta = (int) Math.round(delta);

    // scroll at least one pixel to avoid "hanging"
    // - for "super-low-speed" scrolling (move fingers very slowly on trackpad)
    // - if unit increment is very small (e.g. 1 if scroll view does not implement
    //   javax.swing.Scrollable interface)
    if (idelta == 0) {
        if (rotation > 0) {
            idelta = 1;
        } else if (rotation < 0) {
            idelta = -1;
        }
    }

    // compute new value
    int value = scrollbar.getValue();
    int minValue = scrollbar.getMinimum();
    int maxValue = scrollbar.getMaximum() - scrollbar.getModel().getExtent();
    int newValue = Math.max(minValue, Math.min(value + idelta, maxValue));

    // set new value
    if (newValue != value) {
        scrollbar.setValue(newValue);
    }
}
Federalese answered 17/7, 2023 at 3:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.