Sergiy Medvynskyy
Sergiy Medvynskyy

Reputation: 11327

Layout JPopupMenu: How to provide different size for menu items in same menu

I need two columns of menu items in a menu. This can be achieved using the GridBagLayout for a popup menu. My problem is: both column have the same width which is the width of the largest menu item in the menu (but not in column as it must be for GridBagLayout). When I replace menu items in the menu by buttons - the layout works correct. So how can i advice a menu that the items should have different widths?

Here are the screens:

When I use menu items - both columns have the same width (wrong) Wrong layout for menu items

When I use buttons - columns have different widths (correct) Correct layout for buttons

Here is the code example:

import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.WindowConstants;

public class MenuTest {

    public static void main(String[] args) {
        JFrame frm = new JFrame("Menu test");
        JMenuBar menuBar = new JMenuBar();
        JMenu m1 = new JMenu("Menu items");
        m1.getPopupMenu().setLayout(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 0;
        m1.getPopupMenu().add(new JMenuItem("Very very very very long text"), gbc);
        gbc.gridx = 1;
        m1.getPopupMenu().add(new JMenuItem("Short text"), gbc);
        menuBar.add(m1);

        JMenu m2 = new JMenu("Buttons");
        m2.getPopupMenu().setLayout(new GridBagLayout());
        gbc.gridx = 0;
        m2.getPopupMenu().add(new JButton("Very very very very long text"), gbc);
        gbc.gridx = 1;
        m2.getPopupMenu().add(new JButton("Short text"), gbc);
        menuBar.add(m2);
        frm.setJMenuBar(menuBar);
        frm.add(new JScrollPane(new JTextArea("Sample text", 10, 40)));
        frm.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frm.pack();
        frm.setVisible(true);
    }
}

Upvotes: 2

Views: 1790

Answers (2)

camickr
camickr

Reputation: 324147

All I can say is that the JMenuItem appears to have some weird logic when it comes to calculating the size of the menu item. Looking at the BasicMenuItemUI class it appear that the MenuItemLayoutHelper class is used to help calculate the preferred size of the menu item. This class is found in the sun.swing.* package so I can't look at the code.

Anyway, I did some more testing trying to determine the preferred size of the components as they are added to the panel using the following code:

import java.awt.*;
import javax.swing.*;

public class Main
{
    public static void main(String[] args) throws Exception
    {
        JMenuItem mi1 = new JMenuItem("Very very very long text");
        JMenuItem mi2 = new JMenuItem("Shorter text");
        JButton b1 = new JButton("Very very very long text");
        JButton b2 = new JButton("Shorter text");

        System.out.println("Menu Items:");
        createPanel(mi1, mi2);

        System.out.println("Buttons:");
        createPanel(b1, b2);

        System.out.println("mixture:");
        createPanel(mi2, b1);

    }

    public static void createPanel(Component c1, Component c2)
    {
        System.out.println(c1.getPreferredSize() + " : " + c2.getPreferredSize());

        JPanel panel = new JPanel( new GridBagLayout() );
        GridBagConstraints gbc = new GridBagConstraints();

        gbc.gridx = 0;
        gbc.gridy = 0;
        panel.add(c1, gbc);
        System.out.println("p1: " + panel.getPreferredSize());

        gbc.gridx = 1;
        panel.add(c2, gbc);
        System.out.println("p2: " + panel.getPreferredSize());
    }
}

I get the following output:

Menu Items:
java.awt.Dimension[width=153,height=21] : java.awt.Dimension[width=89,height=21]
p1: java.awt.Dimension[width=153,height=21]
p2: java.awt.Dimension[width=306,height=21]
Buttons:
java.awt.Dimension[width=166,height=26] : java.awt.Dimension[width=102,height=26]
p1: java.awt.Dimension[width=166,height=26]
p2: java.awt.Dimension[width=268,height=26]
mixture:
java.awt.Dimension[width=153,height=21] : java.awt.Dimension[width=166,height=26]
p1: java.awt.Dimension[width=89,height=21]
p2: java.awt.Dimension[width=255,height=26]

In the "Menu Items" example the 2nd menu item has a width of 89 however, it magically gets a width of 153 (which is the same width as the first menu item) when added to the panel.

The "Buttons" example works as expected. The preferred width of each button (166 and 102) is reflected in the panel width of 268.

In the "Mixture" example the menu item width is displayed as 153 the width it was assigned when used in the "menu items" example. However, as soon as the menu item is added the panel it width drops back to 89 which is the expected width.

This tells me that the preferred size of a menu item is actually the size of the largest menu item added to the container.

So if a container (either a JPanel or JPopupMenu) only contains JMenuItems, then all menu items will be same size. If the container contains a mixture of menu items and other components, then the menu item will be displayed at its preferred size (depending on the layout manager of course).

Upvotes: 2

Guillaume Polet
Guillaume Polet

Reputation: 47607

Apparently, JMenuItem layout is based on maximum width of all the contained JMenutItem within the same parent component. After looking at Sun/Oracle's code, it seems very hard to interfere with this and the preferred size of each JMenuItem will be equivalent.

Yet, I found one way to overcome this, by overriding getPreferredSize() of JMenutItem, not pretty, but seems to work:

import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;

import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingConstants;
import javax.swing.WindowConstants;

public class MenuTest {

    public static void main(String[] args) {
        JFrame frm = new JFrame("Menu test");
        JMenuBar menuBar = new JMenuBar();
        JMenu m1 = new JMenu("Menu items");
        m1.getPopupMenu().setLayout(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 0;
        JMenuItem comp1 = new JMenuItem("Very very very very long text") {
            @Override
            public Dimension getPreferredSize() {
                return new JMenuItem(getText(), getIcon()).getPreferredSize();
            }
        };
        m1.getPopupMenu().add(comp1, gbc);
        gbc.gridx = 1;
        JMenuItem comp2 = new JMenuItem("Short text") {
            @Override
            public Dimension getPreferredSize() {
                return new JMenuItem(getText(), getIcon()).getPreferredSize();
            }
        };
        m1.getPopupMenu().add(comp2, gbc);
        menuBar.add(m1);

        JMenu m2 = new JMenu("Buttons");
        m2.getPopupMenu().setLayout(new GridBagLayout());
        gbc.gridx = 0;
        m2.getPopupMenu().add(new JButton("Very very very very long text"), gbc);
        gbc.gridx = 1;
        m2.getPopupMenu().add(new JButton("Short text"), gbc);
        menuBar.add(m2);
        frm.setJMenuBar(menuBar);
        frm.add(new JScrollPane(new JTextArea("Sample text", 10, 40)));
        frm.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frm.pack();
        frm.setVisible(true);
        comp1.setVerticalTextPosition(SwingConstants.TOP);
        comp2.setVerticalTextPosition(SwingConstants.TOP);
    }
}

Of course, if you use other properties of the JMenuItem that modifies the UI of the JMenuItem, you'll need to pass that to the "internal" JMenuItem as well. It's also probably hard to maintain and won't work across all L&F.

You can also probably improve this by re-using the same "internal" JMenuItem across the different calls.

Upvotes: 3

Related Questions