Showing JDialog as sheet not working

As far as I know, Apple didn't officially released their version of JDK 7. The latest version of the JDK Apple has optimized for their OS X is still JDK 6. That is also why updates for Java come thru the AppStore update tab. These updates do not come straight from Oracle.

If you downloaded JDK 7 directly from Oracle, this is a more generic, non-tweaked version.

So, I think you will just have to wait for Apple to release their OS X optimized JDK 7.

I experienced a lot of OS X features not to be working when I download from Oracle:

  • Trackpad gestures
  • Native Aqua Look'n'Feel doesn't work, even when trying to set it manually through UIManager.
  • Application icon not working when using JOptionPane.
  • JMenu's will stick into the JFrame itself instead of moving to the top of the screen.

It seems before JDK fix the bug, you have to implement the Sheet yourself.

The key points are:

  1. Use the glass pane of the window frame to hold the Sheet dialog
  2. Use the GridBagLayout (with NORTH anchor) to put the dialog at top|center of the pane
  3. Animate the Sheet when show/disappeare by painting the dialog repeatedly, every time paint more/less part of the dialog

The following is the example code

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;

import javax.swing.Box;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.border.LineBorder;

public class SheetableJFrame extends JFrame implements ActionListener {

    public static final int INCOMING = 1;
    public static final int OUTGOING = -1;
    public static final float ANIMATION_DURATION = 1000f;
    public static final int ANIMATION_SLEEP = 50;

    JComponent sheet;
    JPanel glass;
    Sheet animatingSheet;
    boolean animating;
    int animationDirection;
    Timer animationTimer;
    long animationStart;
    BufferedImage offscreenImage;

    public SheetableJFrame() {
        super();
        glass = (JPanel) getGlassPane();
        glass.setLayout(new GridBagLayout());
        animatingSheet = new Sheet();
        animatingSheet.setBorder(new LineBorder(Color.black, 1));
    }

    public JComponent showJDialogAsSheet(JDialog dialog) {
        sheet = (JComponent) dialog.getContentPane();
        sheet.setBorder(new LineBorder(Color.black, 1));
        glass.removeAll();
        animationDirection = INCOMING;
        startAnimation();
        return sheet;
    }

    public void hideSheet() {
        animationDirection = OUTGOING;
        startAnimation();
    }

    private void startAnimation() {
        glass.repaint();
        // clear glasspane and set up animatingSheet
        animatingSheet.setSource(sheet);
        glass.removeAll();
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.anchor = GridBagConstraints.NORTH;
        glass.add(animatingSheet, gbc);
        gbc.gridy = 1;
        gbc.weighty = Integer.MAX_VALUE;
        glass.add(Box.createGlue(), gbc);
        glass.setVisible(true);

        // start animation timer
        animationStart = System.currentTimeMillis();
        if (animationTimer == null) animationTimer = new Timer(ANIMATION_SLEEP, this);
        animating = true;
        animationTimer.start();
    }

    private void stopAnimation() {
        animationTimer.stop();
        animating = false;
    }

    // used by the Timer
    public void actionPerformed(ActionEvent e) {
        if (animating) {
            // calculate height to show
            float animationPercent = (System.currentTimeMillis() - animationStart) / ANIMATION_DURATION;
            animationPercent = Math.min(1.0f, animationPercent);
            int animatingHeight = 0;

            if (animationDirection == INCOMING) {
                animatingHeight = (int) (animationPercent * sheet.getHeight());
            } else {
                animatingHeight = (int) ((1.0f - animationPercent) * sheet.getHeight());
            }
            // clip off that much from sheet and put it into animatingSheet
            animatingSheet.setAnimatingHeight(animatingHeight);
            animatingSheet.repaint();

            if (animationPercent >= 1.0f) {
                stopAnimation();
                if (animationDirection == INCOMING) {
                    finishShowingSheet();
                } else {
                    glass.removeAll();
                    glass.setVisible(false);
                    glass.setLayout(new GridBagLayout());
                    animatingSheet = new Sheet();
                }
            }
        }
    }

    private void finishShowingSheet() {
        glass.removeAll();
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.anchor = GridBagConstraints.NORTH;
        glass.add(sheet, gbc);
        gbc.gridy = 1;
        gbc.weighty = Integer.MAX_VALUE;
        glass.add(Box.createGlue(), gbc);
        glass.revalidate();
        glass.repaint();
    }

    class Sheet extends JPanel {
        Dimension animatingSize = new Dimension(0, 1);
        JComponent source;
        BufferedImage offscreenImage;

        public Sheet() {
            super();
            setOpaque(true);
        }

        public void setSource(JComponent source) {
            this.source = source;
            animatingSize.width = source.getWidth();
            makeOffscreenImage(source);
        }

        public void setAnimatingHeight(int height) {
            animatingSize.height = height;
            setSize(animatingSize);
        }

        private void makeOffscreenImage(JComponent source) {
            GraphicsConfiguration gfxConfig = GraphicsEnvironment.getLocalGraphicsEnvironment()
                    .getDefaultScreenDevice().getDefaultConfiguration();
            offscreenImage = gfxConfig.createCompatibleImage(source.getWidth(), source.getHeight());
            Graphics2D offscreenGraphics = (Graphics2D) offscreenImage.getGraphics();
            source.paint(offscreenGraphics);
        }

        public Dimension getPreferredSize() {
            return animatingSize;
        }

        public Dimension getMinimumSize() {
            return animatingSize;
        }

        public Dimension getMaximumSize() {
            return animatingSize;
        }

        public void paint(Graphics g) {
            // get the bottom-most n pixels of source and paint them into g, where n is height
            BufferedImage fragment = offscreenImage.getSubimage(0, offscreenImage.getHeight() - animatingSize.height,
                    source.getWidth(), animatingSize.height);
            g.drawImage(fragment, 0, 0, this);
        }
    }
}

The test code

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.JDialog;
import javax.swing.JOptionPane;

public class SheetTest extends Object implements PropertyChangeListener {

    JOptionPane optionPane;
    SheetableJFrame frame;

    public static void main(String[] args) {
        new SheetTest();
    }

    public SheetTest() {
        frame = new SheetableJFrame();
        // build JOptionPane dialog and hold onto it
        optionPane = new JOptionPane("Do you want to close?", JOptionPane.QUESTION_MESSAGE, JOptionPane.CANCEL_OPTION);
        frame.setSize(640, 480);
        frame.setVisible(true);
        optionPane.addPropertyChangeListener(this);

        JDialog dialog = optionPane.createDialog(frame, "irrelevant");
        frame.showJDialogAsSheet(dialog);
    }

    public void propertyChange(PropertyChangeEvent pce) {
        if (pce.getPropertyName().equals(JOptionPane.VALUE_PROPERTY)) {
            System.out.println("Selected option " + pce.getNewValue());
            frame.hideSheet();
        }
    }
}

reference
http://oreilly.com/pub/h/4852
http://book.javanb.com/swing-hacks/swinghacks-chp-6-sect-6.html