Search code examples
javaswinglook-and-feelnimbusjpopupmenu

Java Popup Button


Note: You may have to compile and run my example to fully understand my question. If this is not kosher, I apologize in advance.

I am trying to create a Swing control that is based on a JToggleButton and a JPopupMenu.

The toggle button is selected iff the popup menu is visible, and the toggle button is deselected iff the popup menu is not visible. Thus, the behavior is similar to a JComboBox, except that the popup can contain arbitrary components.

The code that follows is an example of how I would create the control (except that it would be in its own class... something like a JPopupToggleButton). Unfortunately, it exhibits different behavior under different look and feels (I have tested it with Metal and Nimbus).

The code as posted here behaves as expected in Metal, but not in Nimbus. When using Nimbus, just show and hide the popup by repeatedly clicking the toggle button and you will see what I mean.

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;

public class PopupButtonExample extends JFrame
{
    public static void main( String[] args )
    {
        java.awt.EventQueue.invokeLater( new Runnable()
        {
            @Override
            public void run()
            {
                PopupButtonExample example = new PopupButtonExample();
                example.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
                example.setVisible( true );
            }
        });
    }

    public PopupButtonExample()
    {
        super( "Components in Popup" );

        JPanel popupPanel = new JPanel();
        popupPanel.setLayout( new BorderLayout() );
        popupPanel.add( new JLabel( "This popup has components" ),
                BorderLayout.NORTH );
        popupPanel.add( new JTextArea( "Some text", 15, 20 ),
                BorderLayout.CENTER );
        popupPanel.add( new JSlider(), BorderLayout.SOUTH );

        final JPopupMenu popupMenu = new JPopupMenu();
        popupMenu.add( popupPanel );

        final JToggleButton popupButton = new JToggleButton( "Show Popup" );
        popupButton.addActionListener( new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                if( popupButton.isSelected() )
                    popupMenu.show( popupButton, 0, popupButton.getHeight() );
            }
        });

        popupMenu.addPopupMenuListener( new PopupMenuListener()
        {
            @Override
            public void popupMenuWillBecomeVisible(PopupMenuEvent pme) {}

            @Override
            public void popupMenuCanceled(PopupMenuEvent pme) {}

            @Override
            public void popupMenuWillBecomeInvisible(PopupMenuEvent pme) {
                Point mouseLoc = MouseInfo.getPointerInfo().getLocation();
                Point componentLoc = popupButton.getLocationOnScreen();
                mouseLoc.x -= componentLoc.x;
                mouseLoc.y -= componentLoc.y;
                if( !popupButton.contains( mouseLoc ) )
                    popupButton.setSelected( false );
            }
        });

        JPanel toolBarPanel = new JPanel();
        toolBarPanel.add( popupButton );
        JToolBar toolBar = new JToolBar();
        toolBar.add( toolBarPanel );

        setLayout( new BorderLayout() );
        add( toolBar, BorderLayout.PAGE_START );
        setPreferredSize( new Dimension( 640, 480 ) );
        pack();
    }
}

Commeting out the following lines makes the code behave as expected in Nimbus, but not in Metal. Again, just keep clicking the toggle button to see what I mean.

//                Point mouseLoc = MouseInfo.getPointerInfo().getLocation();
//                Point componentLoc = popupButton.getLocationOnScreen();
//                mouseLoc.x -= componentLoc.x;
//                mouseLoc.y -= componentLoc.y;
//                if( !popupButton.contains( mouseLoc ) )

So here are my two questions:

(1) In Nimbus, why does the click that hides the popup panel not get passed to the toggle button, as it does with Metal?

(2) How can I solve this problem so that it works with all look and feels?


Solution

  • After some investigation, I found the cause for the difference between Nimbus and Metal. The following flag is used (at least by BasicPopupMenuUI) to control the consumption of events when a popup is closed:

    UIManager.getBoolean( "PopupMenu.consumeEventOnClose" );
    

    When using Nimbus, this returns true. When using Metal, this returns false. Thus, the method popupMenuWillBecomeInvisible should be defined as follows:

    if( UIManager.getBoolean( "PopupMenu.consumeEventOnClose" ) )
    {
        popupButton.setSelected( false );
    }
    else
    {
        Point mouseLoc = MouseInfo.getPointerInfo().getLocation();
        Point componentLoc = popupButton.getLocationOnScreen();
        mouseLoc.x -= componentLoc.x;
        mouseLoc.y -= componentLoc.y;
        if( !popupButton.contains( mouseLoc ) )
        {
            popupButton.setSelected( false );
        }
    }