views:

703

answers:

4

I've created a drop-down menu on my Swing JToolBar. But it doesn't create behave the way I want. I'm aiming for it to work like Firefox's "Smart Bookmarks" button.

It disappears when the user selects a menu item: CORRECT!

It disappears when the user presses ESC: CORRECT!

It disappears when the user clicks somewhere in the main frame outside of the menu: CORRECT!

But it doesn't disappear when the user clicks a second time on the button that shows the drop-down menu: INCORRECT... :-(

My question is how can I add this behaviour, that it does disappear when the clicks on the button that shows the menu a second time.

Here's my current code, from Java 6 on the Mac:

import javax.swing.*;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import java.awt.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;

public class ScratchSpace {

    public static void main(String[] arguments) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Toolbar with Popup Menu demo");

                final JToolBar toolBar = new JToolBar();
                toolBar.add(createMoreButton());

                final JPanel panel = new JPanel(new BorderLayout());
                panel.add(toolBar, BorderLayout.NORTH);
                panel.setPreferredSize(new Dimension(600, 400));
                frame.getContentPane().add(panel);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    private static AbstractButton createMoreButton() {
        final JToggleButton moreButton = new JToggleButton("More...");
        moreButton.addItemListener(new ItemListener() {
            public void itemStateChanged(ItemEvent e) {
                if (e.getStateChange() == ItemEvent.SELECTED) {
                    createAndShowMenu((JComponent) e.getSource(), moreButton);
                }
            }
        });
        moreButton.setFocusable(false);
        moreButton.setHorizontalTextPosition(SwingConstants.LEADING);
        return moreButton;
    }

    private static void createAndShowMenu(final JComponent component, final AbstractButton moreButton) {
        JPopupMenu menu = new JPopupMenu();
        menu.add(new JMenuItem("Black"));
        menu.add(new JMenuItem("Red"));

        menu.addPopupMenuListener(new PopupMenuListener() {
            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
            }

            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
                moreButton.setSelected(false);
            }

            public void popupMenuCanceled(PopupMenuEvent e) {
                moreButton.setSelected(false);
            }
        });

        menu.show(component, 0, component.getHeight());
    }
}
A: 

Well, the listener on the button reacts only when it is pushed down, because you listen for ItemEvent.SELECTED events only. How about adding another if clause to listen for ItemEvent.DESELECTED events here:

    moreButton.addItemListener(new ItemListener() {
        public void itemStateChanged(ItemEvent e) {
            if (e.getStateChange() == ItemEvent.SELECTED) {
                createAndShowMenu((JComponent) e.getSource(), moreButton);
            }
        }
    });

You could either store a reference to the menu somewhere, or you could make the menu itself add another listener to the button. The latter solution could be more straightforward, since you already seem to send a button reference to the menu.

Joonas Pulakka
Joonas, have you tried your suggestion? I've tried this and it doesn't seem to work. The Popup menu fires a cancel event on clicking the button, which unselects the button. This ensures that the button is always set to selected when you click on it.
Steve McLeod
I haven't tested exactly the code above, but I'm pretty sure that it should work the way I describe. Maybe I don't understand something, but depending on popupMenuCanceled() event feels fragile, since the documentation only says that "This method is called when the popup menu is canceled". What's that supposed to mean? I suggests just adding an explicit listener for the button's state.
Joonas Pulakka
Joonas, the problem is that your suggestion just doesn't work. Try it and see.
Steve McLeod
Bummer. It seems that the button emits ItemEvent.SELECTED events on weird occasions, such as when calling setSelected(false). So, this is a quite good question, sorry for having no time to dig it deeper just now.
Joonas Pulakka
Well the real issue is that all of the events in PopupListener get called before the JToggleButton even gets notified that it's been clicked. Therefore, when clicking on the toggle button to dismiss the popup it gets deselected by the popuplistener and THEN the button logic itself handles the click, sees that it isn't selected any more and changes the state to selected... thus putting the popup back again.
BryanD
A: 

I don't use Firefox so I don't know what the Smart Bookmarks button looks like, but maybe use a JMenu as the "button". You could try using the Border of a JButton to make it look more like a button.

camickr
This should do the trick
willcodejavaforfood
Tried this, it's not a good result.
Steve McLeod
So you down vote the suggestion because it doesn't look like what you want. Good to know, next time I work give you an "outside of the box" type of suggestion. I got it working so I don't know what problems you are having.
camickr
Camickr, I didn't mean to offend. I downvoted because your answer doesn't work, in that it doesn't give Firefox "Smart Bookmarks" behaviour at all. For the sake of future people with the same question, I vote wrong answers down and right answers up.
Steve McLeod
Like I said I don't know how Smart Bookmarks works (or what it looks like) so I was throwing out a suggestion to get you thinking outside the box. As I said it works perfectly fine for me. You click it and a dropdown appears and you click on a menu item, so I don't know what it is that you don't like about the suggestion.
camickr
+2  A: 

Well, here is a potential solution that is not without it's drawbacks. Only you can decide if this is acceptable for your application. The issue is that the popup closing occurs before other mouse-handling events are fired so clicking on your More.. button again causes the popup to hide, thus resetting the buttons state to deselected BEFORE the button even gets told it was pressed.

The easy workaround is to add the following call within your main program:

UIManager.put("PopupMenu.consumeEventOnClose", Boolean.TRUE);

The result of this is that whenever a popup menu is closed because of a mouse-pressed event, that mouse event will be consumed at the time the menu is closed and won't be passed on to any other components under the mouse. If you can live with limitation, this is an easy solution.

BryanD
Works like a charm... A question: how did you know about this solution? Is it documented somewhere?
Steve McLeod
I was actually just using my debugger (with the JDK src attached) to find out where and when the events were getting fired. I found it in BasicPopupMenuUI.
BryanD
+1  A: 

What's happening is that when you click off the menu, it cancels the popup menu, so you deselect the button, but the next immediate event is clicking the button, and now its deselected so it shows the menu again.

I don't have the exact solution yet, but give me a little bit ...

Milan Ramaiya