Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swing: popover with arrow

I'd like to create a Google Chrome favorites-style popover with arrow, rounded corners and if I have time a shadow effect. In Java Swing. What is the best approach? SplashScreen? Or just a plain AWT Window? Other ideas? Thanks!

like image 975
Jonas Byström Avatar asked Apr 11 '12 06:04

Jonas Byström


1 Answers

There are a few options and each of them has its own pros and cons...

  1. Create a custom-shaped window - with this approach some systems will be able to create additional shade behind the shaped window, also this works on most of systems (should work even on linux JDK's). The bad thing about this approach (that actually makes it unusable) is the unaliased shape border line - if you create some ellipse-shaped window its sides will appear rough.

  2. Create a non-opaque undecorated window with drawn shape - this approach will fix the main problem of the (1) approach. You can alias shape you re drawing on fully transparent window. The bad thing about this one is that it works only on Win and Mac systems. On (mostly) any linux system you will get a rectangle resulting window and tons of errors about unsupported operations.

  3. Create a custom-shaped popup inside the java-window and place it on the window layered or glass panes. This will allow you to fully avoid any compatibility problems and get the benefits of the (2) approach. There is a bad thing about this approach though - you can only display such popup in window root pane bounds. This is still much better than two other ways in most of cases, since it uses less resources, does not create additional windows and you can control every part of the popup.

About the 3rd approach - you can check TooltipManager i have created in my own project WebLookAndFeel - it uses window glass pane to display custom-shaped semi-transparent tooltips with a shadow-effect. Also soon enough i will add window PopupManager that will allow quick creation of "inner" window popups.

Here are some examples of the approaches:

A bit of code that used ahead in all of the examples

Method to create shape:

private static Area createShape ()
{
    Area shape = new Area ( new RoundRectangle2D.Double ( 0, 20, 500, 200, 20, 20 ) );

    GeneralPath gp = new GeneralPath ( GeneralPath.WIND_EVEN_ODD );
    gp.moveTo ( 230, 20 );
    gp.lineTo ( 250, 0 );
    gp.lineTo ( 270, 20 );
    gp.closePath ();
    shape.add ( new Area ( gp ) );

    return shape;
}

Mouse adapter that allows to move window by dragging the component:

public static class WindowMoveAdapter extends MouseAdapter
{
    private boolean dragging = false;
    private int prevX = -1;
    private int prevY = -1;

    public WindowMoveAdapter ()
    {
        super ();
    }

    public void mousePressed ( MouseEvent e )
    {
        if ( SwingUtilities.isLeftMouseButton ( e ) )
        {
            dragging = true;
        }
        prevX = e.getXOnScreen ();
        prevY = e.getYOnScreen ();
    }

    public void mouseDragged ( MouseEvent e )
    {
        if ( prevX != -1 && prevY != -1 && dragging )
        {
            Window w = SwingUtilities.getWindowAncestor ( e.getComponent () );
            if ( w != null && w.isShowing () )
            {
                Rectangle rect = w.getBounds ();
                w.setBounds ( rect.x + ( e.getXOnScreen () - prevX ),
                        rect.y + ( e.getYOnScreen () - prevY ), rect.width, rect.height );
            }
        }
        prevX = e.getXOnScreen ();
        prevY = e.getYOnScreen ();
    }

    public void mouseReleased ( MouseEvent e )
    {
        dragging = false;
    }
}

1st approach example:

public static void main ( String[] args )
{
    JFrame frame = new JFrame ();
    frame.setUndecorated ( true );

    JPanel panel = new JPanel ();
    panel.setBackground ( Color.BLACK );
    WindowMoveAdapter wma = new WindowMoveAdapter ();
    panel.addMouseListener ( wma );
    panel.addMouseMotionListener ( wma );
    frame.getContentPane ().add ( panel );

    Area shape = createShape ();
    AWTUtilities.setWindowShape ( frame, shape );
    frame.setSize ( shape.getBounds ().getSize () );
    frame.setLocationRelativeTo ( null );

    frame.setDefaultCloseOperation ( JFrame.EXIT_ON_CLOSE );
    frame.setVisible ( true );
}

As you can see - corners of the rounded shape are pretty rough and not good-looking

2nd approach:

public static void main ( String[] args )
{
    JFrame frame = new JFrame ();
    frame.setUndecorated ( true );

    final Area shape = createShape ();
    JPanel panel = new JPanel ()
    {
        protected void paintComponent ( Graphics g )
        {
            super.paintComponent ( g );

            Graphics2D g2d = ( Graphics2D ) g;
            g2d.setRenderingHint ( RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON );

            g2d.setPaint ( Color.BLACK );
            g2d.fill ( shape );
        }
    };
    panel.setOpaque ( false );
    WindowMoveAdapter wma = new WindowMoveAdapter ();
    panel.addMouseListener ( wma );
    panel.addMouseMotionListener ( wma );
    frame.getContentPane ().add ( panel );

    AWTUtilities.setWindowOpaque ( frame, false );
    frame.setSize ( shape.getBounds ().getSize () );
    frame.setLocationRelativeTo ( null );

    frame.setDefaultCloseOperation ( JFrame.EXIT_ON_CLOSE );
    frame.setVisible ( true );
}

Now it should look perfect - the only problem this will properly work only on Windows and Mac (atleast in 1.6.x JDK). Atleast it was so about a month ago when i last time checked it on various OS.

3rd approach

public static void main ( String[] args )
{
    JFrame frame = new JFrame ();

    JPanel panel = new JPanel ( new BorderLayout () );
    panel.setOpaque ( false );
    WindowMoveAdapter wma = new WindowMoveAdapter ();
    panel.addMouseListener ( wma );
    panel.addMouseMotionListener ( wma );
    frame.getContentPane ().add ( panel );

    panel.add ( new JButton ( "Test" ) );

    final Area shape = createShape ();

    JPanel glassPane = new JPanel ( null )
    {
        public boolean contains ( int x, int y )
        {
            // This is to avoid cursor and mouse-events troubles
            return shape.contains ( x, y );
        }
    };
    glassPane.setOpaque ( false );
    frame.setGlassPane ( glassPane );
    glassPane.setVisible ( true );

    JComponent popup = new JComponent ()
    {
        protected void paintComponent ( Graphics g )
        {
            super.paintComponent ( g );

            Graphics2D g2d = ( Graphics2D ) g;
            g2d.setRenderingHint ( RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON );

            g2d.setPaint ( Color.BLACK );
            g2d.fill ( shape );
        }
    };
    popup.addMouseListener ( new MouseAdapter ()
    {
        // To block events on the popup
    });
    glassPane.add ( popup );
    popup.setBounds ( shape.getBounds () );
    popup.setVisible ( true );

    frame.setSize ( 800, 500 );
    frame.setLocationRelativeTo ( null );

    frame.setDefaultCloseOperation ( JFrame.EXIT_ON_CLOSE );
    frame.setVisible ( true );
}

This is a simple example of the popup placed on glass-pane. As you can see it exists only inside of the JFrame, but has the aliased side and works properly on any type of OS.

like image 50
Mikle Garin Avatar answered Oct 26 '22 20:10

Mikle Garin