4

Say that I want to make a button in Java in such a way so that when you click on it, a JPopupMenu appears. The relevant code for it appearing is menu.show(button, button.getWidth()/2, button.getHeight()/2);, which makes the JPopupMenu be displayed with its top left corner at the center of the button, as shown below:

Current Swing JPopupMenu

However, I would like to have it so that instead the bottom left corner is at the center of the button, somewhat like what iTunes does (there is a button underneath the bottom left corner, which is the same size as the + button to the left of it):

enter image description here

I tried to make this happen by getting the height of the JPopupMenu and adding it to the y coordinate that the popup menu is being displayed at, but I found out that the JPopupMenu has a height of 0 before it is visible, which doesn't help me since I'm trying to tell the computer where to make it visible. Also, hardcoding in an offset isn't possible because the number of items in the popup isn't necessarily going to be the same.

How can I make it so my JPopupMenu with an unknown height can be displayed so that it's bottom left coordinate matches a given coordinate?

Thunderforge
  • 19,637
  • 18
  • 83
  • 130

3 Answers3

7

Basically, this creates a popup menu and registers it with the component using JComponent#setComponentPopupMenu. This means that I no longer need to monitor for mouse events or make decisions about when to show the popup.

I then override JComponent#getPopupLocation and calculate the location where I want the popup to appear.

Basically, I get the JComponent#getComponentPopupMenu, get it's preferred size and calculate an appropriate offset so that the bottom, left corner now appears in the center of the component...

enter image description here

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class TestPopup02 {

    public static void main(String[] args) {
        new TestPopup02();
    }

    public TestPopup02() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            JPopupMenu menu = new JPopupMenu();
            menu.add(new JMenuItem("Edit Playlist"));
            menu.addSeparator();
            menu.add(new JMenuItem("Check for Available Downloads..."));
            menu.addSeparator();
            menu.add(new JMenuItem("Export..."));
            menu.add(new JMenuItem("Burn Playlist to Disc"));
            menu.add(new JMenuItem("Copy To Play Order"));
            menu.addSeparator();
            menu.add(new JMenuItem("Delete"));
            setComponentPopupMenu(menu);
        }

        @Override
        public Point getPopupLocation(MouseEvent event) {
            // Get the registered popup menu...
            JPopupMenu popup = getComponentPopupMenu();
            // Get the super point, just in case...
            Point pos = super.getPopupLocation(event);
            if (popup != null) {
                // Create a new "point" location
                pos = new Point();
                // get the preferred size of the menu...
                Dimension size = popup.getPreferredSize();
                // Adjust the x position so that the left side of the popup
                // appears at the center of  the component
                pos.x = (getWidth() / 2);
                // Adjust the y position so that the y postion (top corner)
                // is positioned so that the bottom of the popup
                // appears in the center
                pos.y = (getHeight() / 2) - size.height;
            }
            return pos;
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(400, 200);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            // Simply draws a cross in the center of the window, so you 
            // know where the center is...
            int width = getWidth() - 1;
            int height = getHeight() - 1;
            g.drawLine(width / 2, 0, width / 2, height);
            g.drawLine(0, height / 2, width, height / 2);
        }

    }
}

Update with Mac output

enter image description here

Button example

It is unlikely that you will ever find a solution that meets precisely your needs. One of the greatest skills any developer can develop is the ability to take an idea and mold it there needs.

The previous example holds every thing you need, you just need to be able to make the leap from concept to solution.

getPopupLocation is part of the component popup API, so either overriding the method or calling it probably isn't quite what you need (unless you have a dedicated button for the task, which may not be a bad thing), so you would need to adapt the solution to your needs...

enter image description here

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.GridBagLayout;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class TestPopup02 {

    public static void main(String[] args) {
        new TestPopup02();
    }

    public TestPopup02() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private JButton button;
        private JPopupMenu popup;

        public TestPane() {
            popup = new JPopupMenu();
            popup.add(new JMenuItem("Edit Playlist"));
            popup.addSeparator();
            popup.add(new JMenuItem("Check for Available Downloads..."));
            popup.addSeparator();
            popup.add(new JMenuItem("Export..."));
            popup.add(new JMenuItem("Burn Playlist to Disc"));
            popup.add(new JMenuItem("Copy To Play Order"));
            popup.addSeparator();
            popup.add(new JMenuItem("Delete"));

            setLayout(new GridBagLayout());
            button = new JButton("+");
            button.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    popup.pack();
                    Point pos = new Point();
                    // get the preferred size of the menu...
                    Dimension size = popup.getPreferredSize();
                    // Adjust the x position so that the left side of the popup
                    // appears at the center of  the component
                    pos.x = (button.getWidth() / 2);
                    // Adjust the y position so that the y postion (top corner)
                    // is positioned so that the bottom of the popup
                    // appears in the center
                    pos.y = (button.getHeight() / 2) - size.height;
                    popup.show(button, pos.x, pos.y);
                }
            });
            add(button);
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(400, 200);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            // Simply draws a cross in the center of the window, so you 
            // know where the center is...
            int width = getWidth() - 1;
            int height = getHeight() - 1;
            g.drawLine(width / 2, 0, width / 2, height);
            g.drawLine(0, height / 2, width, height / 2);
        }
    }
}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • I think that you've misunderstood the question. I want for it to be so that when you click on a button (or alternatively, click on a certain point), that the JPopupMenu appears with the bottom left corner at that point (as shown in the iTunes menu). Your screenshot shows the bottom left corner of the JPopupMenu as being off the screen, so it couldn't have worked that way. Also, I'm finding your example really hard to follow; I don't understand what the point represents that getPopupLocation is returning. – Thunderforge May 10 '13 at 05:54
  • `getPopupLocation` returns the location of the top/left corner that you want the popup to appear. By default, `JPopupMenu` will auto correct to make sure it's within the screen bounds. Do you just want to keep the popup within the active frame? – MadProgrammer May 10 '13 at 05:57
  • Yes, I would like to keep it within the bounds of the screen. I don't really care if it's within the bounds of the JFrame though, just that the bottom left corner is at the center of the button (although it'd be hard to get that off the JFrame in most cases). I suppose another point of confusion with your answer is that I'm using menu.show() inside of an ActionListener (see my pastebin in the question comments) whereas you're using a completely different method with a subclassed JPanel. – Thunderforge May 10 '13 at 05:59
  • Ah, I see now that your code is making it so that when you click somewhere, the top edge of the JPopupMenu that will appear is at the direct center of the mouse click. Yeah, it seems like something got lost in translation because that's not what I want at all. – Thunderforge May 10 '13 at 06:03
  • I've updated the example to (be less hackey) and produce the results you seem to be more along the lines of what you want. Also, if you can see what the code is "trying" to do, is such a leap to have a try and correct. Very few examples will ever do "exactly" what you want ;) – MadProgrammer May 10 '13 at 06:12
  • Thanks for the changes to the code, and for revising your example! Unfortunately, your TestPopup02 class doesn't work when I try it. I click all around the JPanel, but a popup menu never appears. I'm running Mac OS X if that matters. – Thunderforge May 10 '13 at 06:18
  • I'll have to wait till I get home to have a look :P – MadProgrammer May 10 '13 at 06:19
  • I have no problems. Works with Java 1.7.0_15 on Mac OS X 10.7.5 – MadProgrammer May 10 '13 at 09:41
  • Ah, figured out my problem. It works if you right-click, but I am wanting it to happen when you left-click on the button (I guess I got so fixed on wanting it to left-click that I didn't even consider right-clicking). If we're using setComponentPopupMenu, will that only appear when you right-click or is there a way to change it so it appears on a left-click? – Thunderforge May 10 '13 at 18:55
  • To clarify, the reason I want it to work when you left-click is that that's normally what you do with a JButton, you don't typically right-click a JButton. Again, this is the way that iTunes and some other programs behave when you click on the buttons to add a playlist and such. – Thunderforge May 10 '13 at 20:16
  • The essential idea of positions remains the same. Instead of using the componentPopup API, you would simply use the buttons ActionListener. The means by which the popup is positioned is essentially the same concept – MadProgrammer May 10 '13 at 20:54
  • So how would I modify the code to make it behave this way as an ActionListener (this is what I did in my original code, linked in the question comments)? Popup.getPreferredSize() returns zero when I put it in the actionPerformed() method, unless the popup has previously been made visible. Do I keep the getPopupLocation() method or something? – Thunderforge May 10 '13 at 22:21
  • @Thunderforge: This answer should be accepted. The popup doesn't have to be visible for getPreferredSize() to work... That would be a silly requirement. Layout managers themselves use this property to set sizes of components. – predi Mar 07 '14 at 09:00
  • @Thunderforge `preferredSize` will return 0x0 if nothing has been added to it or you've set the layout to `null` – MadProgrammer Mar 07 '14 at 22:59
0

This is actually much easier than I thought and should work in most cases. I used your pastebin code and played around it a bit. After you call setVisible(true) on the frame I was able to call menu.getPreferredSize(). Just printing it to standard out gave me java.awt.Dimension[width=31,height=62]. This can be done prior to your ActionListener is called so you will be able to make use of height.

If you use a border in your menu you might have to take this into account but the above should work.

cogsmos
  • 806
  • 6
  • 11
  • 2
    This could have been posted as comment not answer – Java Questions May 10 '13 at 05:08
  • Unfortunately, this still results in a height of 0 until it is made visible at least once. – Thunderforge May 10 '13 at 05:09
  • I tried to post it as a comment and it was not allowed sorry. – cogsmos May 10 '13 at 05:14
  • Thunderforge I think pack() might still work but you may not be able to request the height until until the layout manager does it thing. Try grabbing the height at the end of the EventQueue by grabbing it in a SwingUtilities.invokeLater(). You may have to validate() first too. – cogsmos May 10 '13 at 05:20
  • JPopupMenus are not in a JPanel (they're just floating Components) and so there is no layout manager involved. Full code at http://pastebin.com/NXM3GVn7, which you can use to test and see if you can discover a fix. – Thunderforge May 10 '13 at 05:30
0

This answer just serves to backup my comment about popup visibility and getPreferredSize().

Note that you COULD get a zero preferred height for a popup...if it has zero menu items, that would be a logical conclusion.

Again, MadProgrammer's answer should be accepted.

import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;

public class ButtonPopup extends JFrame {

    private JButton button;

    public ButtonPopup() {        
        setLayout(new GridBagLayout());

        button = new JButton("Click Meh~ /o/");
        button.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                JPopupMenu popup = new JPopupMenu();
                popup.add(new JMenuItem("A"));
                popup.add(new JMenuItem("B"));
                popup.add(new JMenuItem("C"));

                // you want this
                int height = popup.getPreferredSize().height;
                popup.show(
                        button, button.getWidth() / 2,
                        -height + (button.getHeight() / 2));
            }
        });

        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(5, 5, 5, 5);
        add(button, gbc);

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        pack();

        setLocationRelativeTo(null);
    }


    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                new ButtonPopup().setVisible(true);
            }
        });
    }
}

P.S.: I don't usually revive long dead questions, but this time I was looking for something similar an this was to first hit in google.

predi
  • 5,528
  • 32
  • 60