Prevent popup menu dismissal

November 10, 2008

action_button
If you’ve ever written your own pop-down style button, then you’ve most certainly run into the following nasty little issue.

A pop-down button should show a menu below its associated button when pressed. When pressed a second time, the menu should be dismissed. You might initially think this is a straight forward issue, but I assure you, it’s not!

You’re inital pass would probably include a MouseListener on a JToggleButton so that you could show the menu if the button was about to be selected (usually, pop-down menus are shown when the associated button is pressed, rather than fully selected). Your listener might look like this:

    private MouseListener createButtonMouseListener() {
        return new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
                // if the popup menu is currently showing, then hide it.
                // else if the popup menu is not showing, then show it.
                if (fPopupMenu.isShowing()) {
                    hidePopupMenu();
                } else {
                    showPopupMenu();
                }
            }
        };
    }

This actually works perfectly – when you press the button, the menu shows; when you press the button again, the menu hides. After thinking about the problem a little more, you might realize the user would like to dismiss the popup menu with an Escape key press, or a click away from the menu. Either of these events should change the button state back to unpressed.

OK – sounds straight forward enough. We need to add a PopupMenuListener to the JPopupMenu so that we can that we can set the button’s selected state to false when the popup menu is canceled. So you’d create a listener that looks something like this:

    private PopupMenuListener createPopupMenuListener() {
        return new PopupMenuListener() {
            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
                // no implementation.
            }
            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
                // no implementation.
            }
            public void popupMenuCanceled(PopupMenuEvent e) {
                // the popup menu has been canceled externally (either by 
                // pressing escape or clicking off of the popup menu). update
                // the button's state to reflect the menu dismissal.
                fButton.setSelected(false);
            }
        };
    }

Everything seems to be going great! Until, that is, you go back and try to toggle the menu by selecting and deselecting the toggle button. When the button is pressed and the menu is showing, and you press the button a second time, the menu disappears for a fraction of a second and then reappears again.

What’s going on?

When the popup menu is showing, and the button is selected, pressing the button causes the popup menu to dismiss (remember that clicking outside a popup menu normally dismisses it). How, then, are we to indicate that clicking the button should not dismiss the popup menu?

Fortunately, this trail has already been blazed by the implementors of JComboBox. Installing the below client property on our button, allows the button to be clicked, without causing the popup menu to be dismissed:

// this is a trick to get hold of the client property which prevents
// closing of the popup when the down arrow is pressed.
JComboBox box = new JComboBox();
Object preventHide = box.getClientProperty("doNotCancelPopup");
putClientProperty("doNotCancelPopup", preventHide);

Here’s the full listing of the PopdownButton I wrote to demonstrate this issue. This isn’t a production ready implementation, but it’s enough to get you started.

Button at rest gear_arrow
Button pressed/selected gear_arrow_white
public class PopdownButton {
    
    private JToggleButton fButton = new JToggleButton();
    private JPopupMenu fPopupMenu = new JPopupMenu();    
    private boolean fShouldHandlePopupWillBecomeInvisible = true;

    public PopdownButton(Icon defaultIcon, Icon pressedAndSelectedIcon) {
        // setup the default button state.
        fButton.setIcon(defaultIcon);
        fButton.setPressedIcon(pressedAndSelectedIcon);
        fButton.setSelectedIcon(pressedAndSelectedIcon);
        fButton.setFocusable(false);
        fButton.putClientProperty("JButton.buttonType", "textured");
        
        // install a mouse listener on the button to hide and show the popup
        // menu as appropriate.
        fButton.addMouseListener(createButtonMouseListener());
        
        // add a popup menu listener to update the button's selection state
        // when the menu is being dismissed.
        fPopupMenu.addPopupMenuListener(createPopupMenuListener());

        // install a special client property on the button to prevent it from
        // closing of the popup when the down arrow is pressed.
        JComboBox box = new JComboBox();
        Object preventHide = box.getClientProperty("doNotCancelPopup");
        fButton.putClientProperty("doNotCancelPopup", preventHide);
    }
    
    private MouseListener createButtonMouseListener() {
        return new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
                // if the popup menu is currently showing, then hide it.
                // else if the popup menu is not showing, then show it.
                if (fPopupMenu.isShowing()) {
                    hidePopupMenu();
                } else {
                    showPopupMenu();
                }
            }
        };
    }
    
    private PopupMenuListener createPopupMenuListener() {
        return new PopupMenuListener() {
            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
                // no implementation.
            }
            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
                // handle this event if so indicated. the only time we don't handle 
                // this event is when the button itself is pressed, the press action
                // toggles the button selected state for us. this case handles when
                // the button has been toggled, but the user clicks outside the
                // button in order to dismiss the menu.
                if (fShouldHandlePopupWillBecomeInvisible) {
                    fButton.setSelected(false);
                }
            }
            public void popupMenuCanceled(PopupMenuEvent e) {
                // the popup menu has been canceled externally (either by 
                // pressing escape or clicking off of the popup menu). update
                // the button's state to reflect the menu dismissal.
                fButton.setSelected(false);
            }
        };
    }
    
    private void hidePopupMenu() {
        fShouldHandlePopupWillBecomeInvisible = false;
        fPopupMenu.setVisible(false);
        fShouldHandlePopupWillBecomeInvisible = true;
    }
    
    private void showPopupMenu() {
        // show the menu below the button, and slightly to the right.
        fPopupMenu.show(fButton, 5, fButton.getHeight());
    }
    
    public JComponent getComopnent() {
        return fButton;
    }
    
    public JPopupMenu getPopupMenu() {
        return fPopupMenu;
    }
}

12 Responses to “Prevent popup menu dismissal”

  1. Harald K. Says:

    Great tip!

    I once tried to fix a similar problem in a project. After half an hour I decided it wasn’t worth the time..
    I wonder why this isn’t documented?

    Some follow up questions that I’ve been trying to figure out lately, kind of related:
    – How can we have the JCheckBoxMenuItem check icon look like the check icon in the menu bar? Is there a better way than using two pngs?
    – Do you know of a way to have JPopupMenus with rounded corners (and less transparency), like the rest of OS X?

    Anyway, thanks a lot for a great blog!

    .k

  2. Ken Says:

    Hi Harald,

    I’m not sure why this isn’t documented…it took me a while to discover it the first time.

    Re. JCheckBoxMenuItem: I haven’t looked into fixing the JCheckBoxMenuItem check-mark image, though maybe Werner Randelshofer has in his Quaqua project. If not, it would be worth requesting an enhancement from Apple here.

    Re. JPopupMenus with rounded corners: I’ve filed an enhancement request with Apple for this. Unfortunately, I’m not aware of any reasonable workarounds for this one.

    -Ken

  3. Harald K. Says:

    Hi Ken,

    Thanks for the quick answer. I’m aware of Werner’s great Quaqua project, but I stopped using it when switching to Java 6.

    After some more hacking, I came up with an easy solution that solves both the issues: Use AWT PopupMenus.

    I created a thin wrapper for JPopupMenu that delegates to AWT, so it may be set as the ComponentPopupMenu.

    It looks much better!

    .k


  4. […] Orr explores an interesting usability side of Swing buttons with popup menus. The solution involves using an unpublished “doNotCancelPopup” client property […]

  5. Paul Taylor Says:

    Great ken, I may use this on my reworked toolbar – although not sure what having a menu like this on a toolbar adds to an application unless you have many windows. I suppose its a way for Mac applications to have menus at the frame level like windows/linux without admitting as much

  6. Ken Says:

    Hi Paul,

    You can use pop-down menus in a variety of places. For example, Apple’s Keynote (and various other iApps) use pop-down menus in the toolbar. Apple’s Finder also uses pop-down menus to access things like the current path and miscellaneous actions.

    If you want to create the Keynote style of pop-down menu using Mac Widgets for Java, use the MacButtonFactory.makeUnifiedToolBarButton(AbstractButton button). The icon that you supply should already include the drop down arrow.

    Your right, though, in that this is somewhat like having menus at the frame level.


  7. Hi,

    I’ve implemented your suggestions using the button’s ActionListener instead of the MouseListener. All fine but if I have two of such buttons in a toolbar and, once a popup menu is visible, I click on the other button, the first one remains selected. To solve the problem I’ve added a PropertyChangeListener to the menu, listening for the “visible” property, in order to force the button to be unselected when visible is false.

    This is the implementation, where createToggleToolBarButton creates a toggle button with the given ActionListener,

    public JToggleButton createToolBarButton ( final JPopupMenu menu )
    {

    final JToggleButton button = createToggleToolBarButton(
    new ActionListener() {
    public void actionPerformed ( ActionEvent e ) {
    if ( ((JToggleButton) e.getSource()).isSelected() )
    menu.show((Component) e.getSource(), 0, ((Component) e.getSource()).getHeight());
    else
    menu.setVisible(false);
    }
    }
    );

    menu.addPopupMenuListener(
    new PopupMenuListener() {
    public void popupMenuWillBecomeVisible ( PopupMenuEvent e ) {
    }
    public void popupMenuWillBecomeInvisible ( PopupMenuEvent e ) {
    }
    public void popupMenuCanceled ( PopupMenuEvent e ) {
    button.setSelected(false);
    }
    }
    );
    menu.addPropertyChangeListener(
    “visible”,
    new PropertyChangeListener() {
    public void propertyChange ( PropertyChangeEvent e ) {
    if ( Boolean.FALSE.equals(e.getNewValue()) )
    button.setSelected(false);
    }
    }
    );

    // Install a special client property on the button to prevent it from
    // closing of the popup when the down arrow is pressed.
    JComboBox box = new JComboBox();
    Object preventHide = box.getClientProperty(“doNotCancelPopup”);

    button.putClientProperty(“doNotCancelPopup”, preventHide);

    return button;

    }

  8. Ken Says:

    Great catch Claudio! I’v updated the code with a slightly different approach than you suggested.

    -Ken

  9. lOlive Says:

    I wish I could control the popup’s hiding, for my JComboBoxes.
    Basically, I want to show several JComboBox’s popup at once:

    At the moment, I cannot get this feature to work correctly.

    Would the “doNotCancelPopup” trick help, in that case?

    Would it help if I made my JComboBoxes to become PopdownButton ?

    • Ken Says:

      It probably would do what you want, but I’d caution you against such behavior. I would only introduce such non-standard behavior if it were of very significant benefit to the user, and I don’t see that benefit here.

      -Ken

      • lOlive Says:

        AFAIK, any JComboBox already comes with the “doNotCancelPopup” property set.
        If I understand correctly, you advice me to use the PopdownButton components, in my case.
        Right?

  10. Joe Says:

    Hi Ken,

    I’m using Swing and the JComboBox is enclosed into the SwingComboBox object, So using the PopdownButton is not possible.
    The problem is that the popup list contains ‘disabled’ items that the user should not be able to select. Clicking one of those items closes the popup which is not a wanted action but the list should stay open. Only clicking an enabled/valid item should dismiss the popup.
    I’ve tried to set the “doNotCancelPopup” property but that doesn’t work. Also I tried to reopen the popup after invalid item selection but that results an undesired flickering when popup closes and opens.
    Do you have any ideas how to implement the desired behavior?


Leave a comment