Question

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

Was it helpful?

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).

OTHER TIPS

.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.Painter;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;

public class PopupTest {

    private JFrame frame = new JFrame();
    private JPanel panel = new JPanel(new BorderLayout()) {
        private static final long serialVersionUID = 1L;

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(500, 500);
        }
    };
    private JButton button = new JButton("Close me");

    public PopupTest() {
        //panel.setOpaque(false);        
        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 < 56; i++) {//only FHD display
                        menu.add(String.valueOf(i));
                    }
                    menu.show(panel, e.getX(), e.getY());
                }
            }
        });
        button.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.exit(0);
            }
        });
        button.setOpaque(false);
        panel.add(button, BorderLayout.SOUTH);
        frame.setLocation(150, 150);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        //frame.add(panel);
        frame.setContentPane(panel);
        frame.setUndecorated(true);
        frame.pack();
        frame.setBackground(new Color(150, 50, 50, 200));
        frame.setVisible(true);
    }

    public static void main(String[] a) {
        try {
            for (UIManager.LookAndFeelInfo laf : UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(laf.getName())) {
                    UIManager.setLookAndFeel(laf.getClassName());
                    UIManager.getLookAndFeelDefaults().put("PopupMenu[Enabled].backgroundPainter",
                            new FillPainter(new Color(127, 255, 191)));
                    UIManager.getLookAndFeelDefaults().put("text", new Color(255, 0, 0));
                    //UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                    //UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
                    //UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new PopupTest();
            }
        });
    }
}

class FillPainter implements Painter<JComponent> {

    private final Color color;

    FillPainter(Color c) {
        color = c;
    }

    @Override
    public void paint(Graphics2D g, JComponent object, int width, int height) {
        g.setColor(color);
        g.fillRect(0, 0, width - 1, height - 1);
    }
}

Thanks Mikle Garin for a great solution, you helped me a lot to solve a similar problem! I would like to share my solution - based on Mikle's - with a little difference, that was significant in my case.

What I was looking for: transparent undecorated Windows behind JPopupMenu (my custom popups are shown with fancy speech-balloon border, so Window behind it should be invisible).

One thing that didn't work well enough with PropertyChangeListener: window appearance gets adjusted AFTER the window is shown on the screen. On Mac OS X 10.10 with java 8 window behind the popup is first shown with a white background and a thin border (L&F default), and gets adjusted a while (about 0.1 - 0.3 sec) later. Pretty annoying.

After looking a while where to place adjustment code I came up with the following simple solution: adjust window RIGHT AFTER the menu is added to the window and BEFORE showing the window. Following sample shows how to extend JPopupMenu to achieve this:

import java.awt.Color;
import java.awt.Window;
import javax.swing.JPopupMenu;
import javax.swing.Popup;
import javax.swing.RootPaneContainer;
import javax.swing.SwingUtilities;
import javax.swing.plaf.PopupMenuUI;

public class MyPopup extends JPopupMenu
{
    public MyPopup()
    {
        super();
        //...
        setUI(new MyPopupMenuUI());
    }

    //...

    private void adjustHeavyWeightWindowIfThereIsOne()
    {
        // Retrieve popup menu window
        // on Windows we won't find it if this popup is inside of parent frame
        // on Mac we may find it even when DefaultLightWeightPopupEnabled is set to true
        final Window ancestor = SwingUtilities.getWindowAncestor(MyPopup.this);
        if (ancestor != null && ancestor.getClass().getCanonicalName().endsWith("HeavyWeightWindow"))
        {
            adjustWindowAppearance(ancestor);
        }
    }

    private void adjustWindowAppearance(Window w)
    {
        w.setBackground(new Color(0, 0, 0, 0));
        if (w instanceof RootPaneContainer)
        {
            ((RootPaneContainer)w).getRootPane().setBorder(null);
            ((RootPaneContainer)w).getRootPane().setBackground(new Color(0, 0, 0, 0));
        }
    }

    class MyPopupMenuUI extends PopupMenuUI
    {
        public Popup getPopup(JPopupMenu popup, int x, int y)
        {
            Popup toreturn = super.getPopup(popup, x, y);
            adjustHeavyWeightWindowIfThereIsOne();
            return toreturn;
        }
    }
}

Actually extending JPopupMenu is not necessary, but setting a custom PopupMenuUI that triggers an adjustment is essential. Window adjustment code can be easily modified to meet particular needs, and can be moved to custom PopupMenuUI.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top