Showing/hiding a JPopupMenu from a JButton; FocusListener not working?

Joanis picture Joanis · Mar 11, 2010 · Viewed 18.5k times · Source

I needed a JButton with an attached dropdown style menu. So I took a JPopupMenu and attached it to the JButton in the way you can see in the code below. What it needs to do is this:

  • show the popup when clicked
  • hide it if clicked a second time
  • hide it if an item is selected in the popup
  • hide it if the user clicks somewhere else in the screen

These 4 things work, but because of the boolean flag I'm using, if the user clicks somewhere else or selects an item, I have to click twice on the button before it shows up again. That's why I tried to add a FocusListener (which is absolutely not responding) to fix that and set the flag false in these cases.

EDIT: Last attempt in an answer post...

Here are the listeners: (It's in a class extending JButton, so the second listener is on the JButton.)

// Show popup on left click.
menu.addFocusListener(new FocusListener() {
 @Override
 public void focusLost(FocusEvent e) {
  System.out.println("LOST FOCUS");
  isShowingPopup = false;
 }

 @Override
 public void focusGained(FocusEvent e) {
  System.out.println("GAINED FOCUS");
 }
});

addActionListener(new ActionListener() {
 @Override
 public void actionPerformed(ActionEvent e) {
  System.out.println("isShowingPopup: " + isShowingPopup);
  if (isShowingPopup) {
   isShowingPopup = false;
  } else {
   Component c = (Component) e.getSource();
   menu.show(c, -1, c.getHeight());
   isShowingPopup = true;
  }
 }
});

I've been fighting with this for way too long now. If someone can give me a clue about what's wrong with this, it would be great!

Thanks!

Code:

public class Button extends JButton {

    // Icon.
    private static final ImageIcon ARROW_SOUTH = new ImageIcon("ArrowSouth.png");

    // Unit popup menu.
    private final JPopupMenu menu;

    // Is the popup showing or not?
    private boolean isShowingPopup = false;

    public Button(int height) {
        super(ARROW_SOUTH);
        menu = new JPopupMenu(); // menu is populated somewhere else

        // FocusListener on the JPopupMenu
        menu.addFocusListener(new FocusListener() {
            @Override
            public void focusLost(FocusEvent e) {
                System.out.println("LOST FOCUS");
                isShowingPopup = false;
            }

            @Override
            public void focusGained(FocusEvent e) {
                System.out.println("GAINED FOCUS");
            }
        });

        // ComponentListener on the JPopupMenu
        menu.addComponentListener(new ComponentListener() {
            @Override
            public void componentShown(ComponentEvent e) {
                System.out.println("SHOWN");
            }

            @Override
            public void componentResized(ComponentEvent e) {
                System.out.println("RESIZED");
            }

            @Override
            public void componentMoved(ComponentEvent e) {
                System.out.println("MOVED");
            }

            @Override
            public void componentHidden(ComponentEvent e) {
                System.out.println("HIDDEN");
            }
        });

        // ActionListener on the JButton
        addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("isShowingPopup: " + isShowingPopup);
                if (isShowingPopup) {
                    menu.requestFocus();
                    isShowingPopup = false;
                } else {
                    Component c = (Component) e.getSource();
                    menu.show(c, -1, c.getHeight());
                    isShowingPopup = true;
                }
            }
        });

        // Skip when navigating with TAB.
        setFocusable(true); // Was false first and should be false in the end.

        menu.setFocusable(true);
    }

}

Answer

Joanis picture Joanis · Mar 11, 2010

Here's a variant of Amber Shah's "big hack" suggestion I just made. Without the isShowingPopup flag...

It's not bulletproof, but it works quite well until someone comes in with an incredibly slow click to close the popup (or a very fast second click to reopen it...).

public class Button extends JButton {

 // Icon.
 private static final ImageIcon ARROW_SOUTH = new ImageIcon("ArrowSouth.png");

 // Popup menu.
 private final JPopupMenu menu;

 // Last time the popup closed.
 private long timeLastShown = 0;

 public Button(int height) {
  super(ARROW_SOUTH);
  menu = new JPopupMenu(); // Populated somewhere else.

  // Show and hide popup on left click.
  menu.addPopupMenuListener(new PopupMenuListener() {
   @Override
   public void popupMenuWillBecomeInvisible(PopupMenuEvent arg0) {
    timeLastShown = System.currentTimeMillis();
   }
   @Override public void popupMenuWillBecomeVisible(PopupMenuEvent arg0) {}
   @Override public void popupMenuCanceled(PopupMenuEvent arg0) {}
  });
  addActionListener(new ActionListener() {
   @Override
   public void actionPerformed(ActionEvent e) {
    if ((System.currentTimeMillis() - timeLastShown) > 300) {
     Component c = (Component) e.getSource();
     menu.show(c, -1, c.getHeight());
    }
   }
  });

  // Skip when navigating with TAB.
  setFocusable(false);
 }

}

As I said in comments, that's not the most elegant solution, but it's horribly simple and it works in 98% of the cases.

Open to suggestions!