Search code examples
javaswingpopupjpopupmenu

Java JPopupMenu bug


Seems like I've found a bug in Java:

I need to create the JFrame with a transparent background and now I need to show the JPopupMenu for some user actions. It works fine when JPopupMenu is housed fully inside a JFrame. But when the JPopupMenu is partly outside the JFrame, no item is visible.

SSCCE:

public class PopupTest {
    public static void main(String[] a) {
        final JFrame frame = new JFrame();
        frame.setSize(500, 500);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        final JPanel panel = new JPanel(new BorderLayout());
        panel.setBorder(BorderFactory.createLineBorder(Color.RED));

        panel.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.getButton() == MouseEvent.BUTTON3) {
                    JPopupMenu menu = new JPopupMenu();
                    for (int i = 0 ; i < 10; i++) {
                        menu.add(String.valueOf(i));
                    }

                    menu.show(panel, e.getX(), e.getY());
                }
            }
        });
        frame.setContentPane(panel);
        frame.setUndecorated(true);
        frame.setBackground(new Color(50, 50, 50, 200));

        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                frame.setVisible(true);
            }
        });
    }
}

Does anyone know how to solve this?

PS: JDK 7u40, Win x64


Solution

  • This is bug in Oracle JDK 7 (it cannot be reproduced in Open JDK 7 by the way).

    To fix this you can make a workaround (yes, this is just a workaround, there is no guarantees that it won't break with some Java update) so that the window created for popup-menu will become non-opaque as soon as it shows up, then it will be displayed properly. Atleast for now. Here is how this can be done for Java version 7 and later:

    PropertyChangeListener propertyChangeListener = new PropertyChangeListener ()
    {
        @Override
        public void propertyChange ( final PropertyChangeEvent evt )
        {
            if ( evt.getNewValue () == Boolean.TRUE )
            {
                // Retrieving popup menu window (we won't find it if it is inside of parent frame)
                final Window ancestor = getWindowAncestor ( popupMenu );
                if ( ancestor != null && ancestor.getClass ().getCanonicalName ().endsWith ( "HeavyWeightWindow" ) )
                {
                    // Checking that parent window for our window is opaque, only then setting opacity
                    final Component parent = ancestor.getParent ();
                    if ( parent != null && parent instanceof Window && parent.getBackground ().getAlpha () == 0 )
                    {
                        // Making popup menu window non-opaque
                        ancestor.setBackground ( new Color ( 0, 0, 0, 0 ) );
                    }
                }
            }
        }
    
        private Window getWindowAncestor ( Component component )
        {
            if ( component == null )
            {
                return null;
            }
            if ( component instanceof Window )
            {
                return ( Window ) component;
            }
            for ( Container p = component.getParent (); p != null; p = p.getParent () )
            {
                if ( p instanceof Window )
                {
                    return ( Window ) p;
                }
            }
            return null;
        }
    };
    popupMenu.addPropertyChangeListener ( "visible", propertyChangeListener );
    

    You will have to put some more effort if you also want to support JDK 6- versions because that code won't even compile on earlier JDK versions (there are no "set/getBackground" methods in Window in earlier versions).