views:

746

answers:

6

Hi everyone,

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);
    }

}
A: 

You could use the JPopupMenu.isVisible() instead of your Boolean variable to check the current state of the popup menu.

Chad Okere
I tried to do that at first, but the problem is this:As soon as you click on the button (or anywhere else) while the popup is visible, the popup is automatically immediately closed. So isVisible() returns false no matter what.This problem (obviously) also applies to isFocusOwner() and isShowing().
M. Joanis
A: 

Well, I can't be sure without seeing all of your code, but is it possible that the popup never actually gets focus at all? I've had problems with things' not getting focus properly in Swing before, so it could be the culprit. Try calling setFocusable(true) on the menu and then calling requestFocus() when you make the menu appear.

Tikhon Jelvis
Just tried; not working either. I'll post the code in a second.
M. Joanis
A: 

Have you tried adding a ComponentListener to the JPopupMenu, so that you know when it's been shown and hidden (and update your isShowingPopup flag accordingly)? I'm not sure listening for focus changes is necessarily the right approach.

Ash
This seems to be a very good idea, but just like the FocusListener, the ComponentListener is not responding (well, only once: when the popup is shown the first time I get a call to "resized"). I put a println("<method name>") call in each of its methods and nothing is ever printed. I'll post the full code.
M. Joanis
A: 

What you need is a PopupMenuListener:

        menu.addPopupMenuListener(new PopupMenuListener() {

            @Override
            public void popupMenuWillBecomeVisible(PopupMenuEvent arg0) {

            }

            @Override
            public void popupMenuWillBecomeInvisible(PopupMenuEvent arg0) {
                System.out.println("MENU INVIS"); 
                isShowingPopup = false;     
            }

            @Override
            public void popupMenuCanceled(PopupMenuEvent arg0) {
                System.out.println("MENU CANCELLED"); 
                isShowingPopup = false;                     
            }
        });

I inserted this into your code and verified that it works.

Amber Shah
Thank you for your input. I tried this approach already... This fixes the problems discussed above, but creates a new one. With this, we can't close the popup clicking on the button, because it will open no matter if it's closed or opened at first.
M. Joanis
When the popup menu becomes visible, you could modify the action listener on the button so that it won't open the popup (ie, just remove the listener). Then when the menu becomes invisible, set it back (add the listener back).
Dave
You are correct. I'm not sure if there is a way to do this "correctly" since the JPopupMenu coalesces all of the other mouse events into the PopupMenuEvent. Here is a (BIG) hack: save the System.currentTimeMillis() inside each popupMenuWillBecomeInvisible event, and then inside the actionPerformed, do "if(isShowingPopup || (System.currentTimeMillis()-savedTime) < 100) then show... Ok, I'm not suggesting you keep it this way but if it has to work like that... The only other thing I can think of it write your own JPopupMenu implementation and handle the mouse events the way you want to there.
Amber Shah
The problem with Dave's suggestion is that since the popupMenuWillBecomeInvisible is called before actionPerformed on the button, it behaves as if you never removed/added the action listener.
Amber Shah
I was trying to find something better than the "big hack", but if it just fixes the problem and we can't think of something else, I'll go with it. It's not that big of a hack IMO. I agree it's not a nice way of doing it, but since everything else seems "bugged" (FocusListeners just don't work!). That's a very few lines of isolated "ugly" code to be happy and move on to something else... I'll wait a couple of days and see.
M. Joanis
A: 

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!

M. Joanis
+2  A: 

Here is another approach which is not too bad of a hack, if not elegant, and which, as far as I could tell, works. First, at the very top, I added a second boolean called showPopup.

The FocusListener has to be as follows:

    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");
            isShowingPopup = true;
        }
    });

The isShowingPopup boolean does not get changed anywhere else--if it gains focus, it assumes it's shown and if it loses focus, it assumes it isn't.

Next, the ActionListener on the button is different:

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

Now comes the really new bit. It's a MouseListener on the button:

    addMouseListener(new MouseAdapter() {
        @Override
        public void mousePressed(MouseEvent e) {
            System.out.println("ispopup?: " + isShowingPopup);
            if (isShowingPopup) {
                showPopup = false;
            }
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            showPopup = true;
        }
    });

Basically, mousePressed gets called before the menu loses focus, so isShowingPopup reflects whether the popup was shown before the button is pressed. Then, if the menu was there, we just set showPopup to false, so that the actionPerformed method does not show the menu once it gets called (after the mouse is let go).

This behaved as expected in every case but one: if the menu was showing and the user pressed the mouse on the button but released it outside of it, actionPerformed was never called. This meant that showPopup remained false and the menu was not shown the next time the button was pressed. To fix this, the mouseReleased method resets showPopup. The mouseReleased method gets called after actionPerformed, as far as I can tell.

I played around with the resulting button for a bit, doing all the things I could think of to the button, and it worked as expected. However, I am not 100% sure that the events will always happen in the same order.

Ultimately, I think this is, at least, worth trying.

Tikhon Jelvis
Wow, I don't know why the FocusListener works now (I called requestFocus() too!)... but I just tested all this and it seems to work perfectly! That's exactly what was missing! Good work! Thank you very much!
M. Joanis
I think the problem lay in where you called requestFocus()--I called it just after menu.show(), which made it get focus just after being shown, while you called it in the block which fired if the menu was already showing "if(isShowingPopup)..." which makes it try to get focus at the wrong time.
Tikhon Jelvis
Won't do the test, you're too probably right since it works now! Thanks again.
M. Joanis