JDesktopPane preferred size set by content

2.5k views Asked by At

I have been trying to tame JDesktopPane to work nicely with a resizable GUI & a scroll pane, but am having some troubles doing so. It seems that unless the drag mode is outline, the desktop pane will not resize as expected (when an internal frame is dragged beyond the edge of the desktop pane) & therefore not produce scroll-bars.

enter image description here

Am I doing something very silly in this source? Have I missed a far better approach?

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;

public class MDIPreferredSize {

    public static void main(String[] args) {
        Runnable r = new Runnable() {

            @Override
            public void run() {
                final JDesktopPane dt = new JDesktopPane() {

                    @Override
                    public Dimension getPreferredSize() {
                        Dimension prefSize = super.getPreferredSize();
                        System.out.println("prefSize: " + prefSize);
                        // inititialize the max to the first normalized bounds
                        Rectangle max = getAllFrames()[0].getNormalBounds();
                        for (JInternalFrame jif : this.getAllFrames()) {
                            max.add(jif.getNormalBounds());
                        }
                        System.out.println("maxBounds(): "
                                + max);
                        int x1 = max.width + (max.x * 2) < prefSize.width
                                ? prefSize.width
                                : max.width + (max.x * 2);
                        int y1 = max.height + (max.y * 2) < prefSize.height
                                ? prefSize.height
                                : max.height + (max.y * 2);
                        System.out.println("x,y: "
                                + x1
                                + ","
                                + y1);
                        return new Dimension(x1, y1);
                    }
                };
                dt.setAutoscrolls(true);

                int xx = 5;
                int yy = 5;
                int vStep = 10;
                int yStep = 22;
                for (int ii = 0; ii < 3; ii++) {
                    JInternalFrame jif = new JInternalFrame(
                            "Internal Frame " + (ii + 1),
                            true,
                            true,
                            true);
                    dt.add(jif);
                    jif.setLocation(xx, yy);
                    xx += vStep;
                    yy += yStep;
                    jif.setSize(200, 75);
                    jif.setVisible(true);
                }

                ComponentListener componentListener = new ComponentListener() {

                    @Override
                    public void componentResized(ComponentEvent e) {
                        e.getComponent().validate();
                    }

                    @Override
                    public void componentMoved(ComponentEvent e) {
                        e.getComponent().validate();
                    }

                    @Override
                    public void componentShown(ComponentEvent e) {
                        e.getComponent().validate();
                    }

                    @Override
                    public void componentHidden(ComponentEvent e) {
                        // do nothing 
                    }
                };
                // causes maximized internal frames to be resized..
                dt.addComponentListener(componentListener);

                final JCheckBox outLineDragMode = new JCheckBox("Outline Drag Mode");
                ActionListener dragModeListener = new ActionListener() {

                    @Override
                    public void actionPerformed(ActionEvent e) {
                        if (outLineDragMode.isSelected()) {
                            dt.setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);
                        } else {
                            dt.setDragMode(JDesktopPane.LIVE_DRAG_MODE);
                        }
                    }
                };
                outLineDragMode.addActionListener(dragModeListener);

                JPanel gui = new JPanel(new BorderLayout());
                gui.add(outLineDragMode, BorderLayout.PAGE_START);
                gui.setBorder(new EmptyBorder(2, 3, 2, 3));
                gui.add(new JScrollPane(dt), BorderLayout.CENTER);

                JFrame f = new JFrame("DTP Preferred");
                f.add(gui);
                // Ensures JVM closes after frame(s) closed and
                // all non-daemon threads are finished
                f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
                // See http://stackoverflow.com/a/7143398/418556 for demo.
                f.setLocationByPlatform(true);

                // ensures the frame is the minimum size it needs to be
                // in order display the components within it
                f.pack();
                f.setMinimumSize(f.getSize());

                // should be done last, to avoid flickering, moving,
                // resizing artifacts.
                f.setVisible(true);

                printProperty("os.name");
                printProperty("java.version");
                printProperty("java.vendor");
            }
        };
        // Swing GUIs should be created and updated on the EDT
        // http://docs.oracle.com/javase/tutorial/uiswing/concurrency/initial.html
        SwingUtilities.invokeLater(r);
    }

    public static void printProperty(String name) {
        System.out.println(name + ": \t" + System.getProperty(name));
    }
}

Edit

Amongst the information printed, see also the 3 system properties:

os.name:    Windows 7
java.version:   1.7.0_21
java.vendor:    Oracle Corporation

Those are the values here.

MouseMotionListener fixed code

Thanks to Jonathan Drapeau's suggestion of a MouseListener, this fixed example actually uses a MouseMotionListener to allow the desktop pane to be resized actively while dragging. It might suffer some quirks beyond use of a MouseListener that cause problems (none yet known), if so, go back to the simpler technique of 'resize desktop pane on internal frame drop' (MouseListener only).

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.basic.BasicInternalFrameTitlePane;

public class MDIPreferredSize {

    public static void main(String[] args) {
        Runnable r = new Runnable() {

            @Override
            public void run() {
                final JDesktopPane dt = new JDesktopPane() {

                    @Override
                    public Dimension getPreferredSize() {
                        Dimension prefSize = super.getPreferredSize();
                        System.out.println("prefSize: " + prefSize);
                        // inititialize the max to the first normalized bounds
                        Rectangle max = getAllFrames()[0].getNormalBounds();
                        for (JInternalFrame jif : this.getAllFrames()) {
                            max.add(jif.getNormalBounds());
                        }
                        System.out.println("maxBounds(): "
                                + max);
                        int x1 = max.width + (max.x * 2) < prefSize.width
                                ? prefSize.width
                                : max.width + (max.x * 2);
                        int y1 = max.height + (max.y * 2) < prefSize.height
                                ? prefSize.height
                                : max.height + (max.y * 2);
                        System.out.println("x,y: "
                                + x1
                                + ","
                                + y1);
                        return new Dimension(x1, y1);
                    }
                };

                int xx = 5;
                int yy = 5;
                int vStep = 10;
                int yStep = 22;
                for (int ii = 0; ii < 3; ii++) {
                    JInternalFrame jif = new JInternalFrame(
                            "Internal Frame " + (ii + 1),
                            true,
                            true,
                            true);
                    dt.add(jif);
                    jif.setLocation(xx, yy);
                    xx += vStep;
                    yy += yStep;
                    jif.setSize(200, 75);
                    jif.setVisible(true);
                }

                /*final MouseListener mouseListener = new MouseAdapter() {

                    @Override
                    public void mouseReleased(MouseEvent e) {
                        dt.revalidate();
                    }
                };
                */
                final MouseMotionListener mouseMotionListener = new MouseMotionAdapter() {

                    @Override
                    public void mouseDragged(MouseEvent e) {
                        dt.revalidate();
                    }
                };
                for (JInternalFrame jif : dt.getAllFrames()) {
                    for (Component comp : jif.getComponents()) {
                        if (comp instanceof BasicInternalFrameTitlePane) {
                            //comp.addMouseListener(mouseListener);
                            comp.addMouseMotionListener(mouseMotionListener);
                        }
                    }
                }

                dt.setAutoscrolls(true);


                final JCheckBox outLineDragMode =
                        new JCheckBox("Outline Drag Mode");
                ActionListener dragModeListener = new ActionListener() {

                    @Override
                    public void actionPerformed(ActionEvent e) {
                        if (outLineDragMode.isSelected()) {
                            dt.setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);
                        } else {
                            dt.setDragMode(JDesktopPane.LIVE_DRAG_MODE);
                        }
                    }
                };
                outLineDragMode.addActionListener(dragModeListener);

                JPanel gui = new JPanel(new BorderLayout());
                gui.add(outLineDragMode, BorderLayout.PAGE_START);
                gui.setBorder(new EmptyBorder(2, 3, 2, 3));
                gui.add(new JScrollPane(dt), BorderLayout.CENTER);

                JFrame f = new JFrame("DTP Preferred");
                f.add(gui);
                // Ensures JVM closes after frame(s) closed and
                // all non-daemon threads are finished
                f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
                // See http://stackoverflow.com/a/7143398/418556 for demo.
                f.setLocationByPlatform(true);

                // ensures the frame is the minimum size it needs to be
                // in order display the components within it
                f.pack();
                f.setMinimumSize(f.getSize());

                // should be done last, to avoid flickering, moving,
                // resizing artifacts.
                f.setVisible(true);

                printProperty("os.name");
                printProperty("java.version");
                printProperty("java.vendor");
            }
        };
        // Swing GUIs should be created and updated on the EDT
        // http://docs.oracle.com/javase/tutorial/uiswing/concurrency/initial.html
        SwingUtilities.invokeLater(r);
    }

    public static void printProperty(String name) {
        System.out.println(name + ": \t" + System.getProperty(name));
    }
}

Quirks

It might suffer some quirks beyond use of a MouseListener that cause problems (none yet known).

That was then..

  1. In full render mode, the desktop pane will grow dynamically as far as the user drags the internal frame (even off the GUI). (Good.) In outline mode, the container will only resize on drop, not drag. (Less good, but at least the scroll-bars appear/disappear reliably.)
3

There are 3 answers

2
Jonathan Drapeau On BEST ANSWER

Adding a MouseListener to the JInternalFrame title pane while in JDesktopPane.LIVE_DRAG_MODE to revalidate the JDesktopPane after release is a way to get the exact same behavior in each mode.

            final MouseListener testList = new MouseListener() {

              @Override
              public void mouseReleased(MouseEvent e) {
                dt.revalidate();
              }

              @Override
              public void mousePressed(MouseEvent e) {
              }

              @Override
              public void mouseExited(MouseEvent e) {
              }

              @Override
              public void mouseEntered(MouseEvent e) {
              }

              @Override
              public void mouseClicked(MouseEvent e) {
              }
            };
            // causes maximized internal frames to be resized..
            dt.addComponentListener(componentListener);

            for (JInternalFrame jif : dt.getAllFrames()) {
              for (Component comp : jif.getComponents()) {
                if (comp instanceof BasicInternalFrameTitlePane) {
                  comp.addMouseListener(testList);
                }
              }
            }        

            final JCheckBox outLineDragMode = new JCheckBox("Outline Drag Mode");
            ActionListener dragModeListener = new ActionListener() {

                @Override
                public void actionPerformed(ActionEvent e) {
                  if (outLineDragMode.isSelected()) {
                    dt.setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);
                    for (JInternalFrame jif : dt.getAllFrames()) {
                      for (Component comp : jif.getComponents()) {
                        if (comp instanceof BasicInternalFrameTitlePane) {
                          comp.removeMouseListener(testList);
                        }
                      }
                    }
                  } else {
                    dt.setDragMode(JDesktopPane.LIVE_DRAG_MODE);
                    for (JInternalFrame jif : dt.getAllFrames()) {
                      for (Component comp : jif.getComponents()) {
                        if (comp instanceof BasicInternalFrameTitlePane) {
                          comp.addMouseListener(testList);
                        }
                      }
                    }
                  }
                }
            };

I remove them in the JDesktopPane.OUTLINE_DRAG_MODE since it already reacts properly.

0
kleopatra On

Interesting problem for a Saturday morning :-)

No complete solution, just a couple of comments and an outline of an alternative approach:

  • relying on the mouse/Motion/listener is incomplete in that it doesn't handle keyboard controlled moves
  • per-internalframe componentListener to the rescue: works fine if not in outline mode
  • in outline mode the revalidation can't work anyway, because it relies on the actual frame location which is unchanged during the drag

So the real problem is the outline mode, needs to

  • trace the intermediate bounds of the dragged frame
  • let the desktop's prefSize calculation take those intermediate bounds into account
  • the drawing of the outline (unexpected, for me, see below [*])

The collaborator that is responsible for moving the frame is the DesktopManager.dragFrame: it's default implementation resets the frame bounds if not in outline mode or keeps track of the intermediate location and drawing the outline rectangle if in outline mode.

The obvious idea is a custom DesktopManager which overrides dragFrame:

  • let super do its stuff
  • on outline mode, get the frame's intermediate location and store it somewhere on the frame itself, f.i. as a clientProperty

Now a someone, f.i. a PropertyChangeListener can listen for changes of the intermediate location and trigger a revalidate. And the prefSize calculation of the desktopPane can account for the intermediate bounds in addition to the real bounds, something like

public static class MyDesktopManager extends DefaultDesktopManager {
    private Point currentLoc;

    @Override
    public void dragFrame(JComponent f, int newX, int newY) {
        // let super handle outline drawing
        super.dragFrame(f, newX, newY);
        if (isOutline(f)) {
            // take over the drawing
            currentLoc = new Point(newX, newY);
            Rectangle bounds = new Rectangle(currentLoc, f.getSize());
            f.putClientProperty("outlineBounds", bounds);
        } else {
            // call super only if not outline
            // handle outline drawing ourselves
            // super.dragFrame(f, newX, newY);
        }
    }

    @Override
    public void beginDraggingFrame(JComponent f) {
        super.beginDraggingFrame(f);
        if (isOutline(f)) {
            currentLoc = f.getLocation();
            RootPaneContainer r = (RootPaneContainer) SwingUtilities.getWindowAncestor(f);
            // do the painting in the glassPane
            // r.getGlassPane().setVisible(true);
        }
    }

    @Override
    public void endDraggingFrame(JComponent f) {
        super.endDraggingFrame(f);
        f.putClientProperty("outlineBounds", null);
        if (isOutline(f)) {
            RootPaneContainer r = (RootPaneContainer) SwingUtilities.getWindowAncestor(f);
            r.getGlassPane().setVisible(false);
        }
    }

    protected boolean isOutline(JComponent f) {
        return ((JInternalFrame) f).getDesktopPane().getDragMode() == 
             JDesktopPane.OUTLINE_DRAG_MODE;
    }
}

Usage:

final JDesktopPane dt = new JDesktopPane() {

    @Override
    public Dimension getPreferredSize() {
        Dimension prefSize = super.getPreferredSize();
        System.out.println("prefSize: " + prefSize);
        // inititialize the max to the first normalized bounds
        Rectangle max = getAllFrames()[0].getNormalBounds();
        for (JInternalFrame jif : this.getAllFrames()) {
            max.add(jif.getNormalBounds());
            Rectangle outline = (Rectangle) jif.getClientProperty("outlineBounds");
            if (outline != null) {
                max.add(outline);
            }
        }
        int x1 = max.width + (max.x * 2) < prefSize.width ? prefSize.width
                : max.width + (max.x * 2);
        int y1 = max.height + (max.y * 2) < prefSize.height ? prefSize.height
                : max.height + (max.y * 2);
        return new Dimension(x1, y1);
    }
};
dt.setDesktopManager(new MyDesktopManager());
dt.setAutoscrolls(true);
int xx = 5;
int yy = 5;
int vStep = 10;
int yStep = 22;

// oer-internalframe componentListener
ComponentListener il = new ComponentAdapter() {

    @Override
    public void componentMoved(ComponentEvent e) {
        dt.revalidate();
    }

};
// per-internalframe outlineListener
PropertyChangeListener propertyL = new PropertyChangeListener() {

    @Override
    public void propertyChange(PropertyChangeEvent evt) {
        dt.revalidate();
    }
};
for (int ii = 0; ii < 3; ii++) {
    JInternalFrame jif = new JInternalFrame(
            "Internal Frame " + (ii + 1),
            true,
            true,
            true);
    dt.add(jif);
    jif.addComponentListener(il);
    jif.addPropertyChangeListener("outlineBounds", propertyL);
    jif.setLocation(xx, yy);
    xx += vStep;
    yy += yStep;
    jif.setSize(200, 75);
    jif.setVisible(true);
}

[*] The default outline painting flickers (to the extend that it is invisible) - reason being that the default implementation uses ... getGraphics() ... So we need to take over the outline painting, f.i. in a dedicated glassPane (that's done by the commented code) or probably better by a LayerUI on the desktop.

A crude glassPane, just as a poc which doesn't clip correctly and has some issues when the frame is moved back into the visible rect:

public static class OutlinePanel extends JPanel {

    private JDesktopPane desktop;

    public OutlinePanel(JDesktopPane desktop) {
        this.desktop = desktop;
    }

    @Override
    public boolean isOpaque() {
        return false;
    }

    @Override
    protected void paintComponent(Graphics g) {
        JInternalFrame selected = desktop.getSelectedFrame();
        Rectangle outline = (Rectangle) selected.getClientProperty("outlineBounds");
        if (outline == null) return;
        Rectangle bounds = SwingUtilities.convertRectangle(desktop, outline, this);
        g.drawRect(bounds.x, bounds.y, bounds.width, bounds.height);
    }

}

Update

Version with LayerUI - now we are leaving the complete new behaviour (listener registration, painting outline if needed, installing the manager) to the decoration. Advantages:

  • simplified usage
  • one location for all dirty details

The LayerUI:

public class DesktopLayerUI extends LayerUI<JDesktopPane> {

    @Override
    public void installUI(JComponent c) {
        super.installUI(c);
        final JDesktopPane dt = getDesktopPane(c);
        //dt.setBorder(BorderFactory.createLineBorder(Color.RED));
        dt.setDesktopManager(new MyDesktopManager());
        // per-internalframe componentListener
        ComponentListener il = new ComponentAdapter() {

            @Override
            public void componentMoved(ComponentEvent e) {
                dt.revalidate();
            }

        };
        // per-internalframe outlineListener
        PropertyChangeListener propertyL = new PropertyChangeListener() {

            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                dt.revalidate();
            }
        };
        for (JInternalFrame jif : dt.getAllFrames()) {
            jif.addComponentListener(il);
            jif.addPropertyChangeListener("outlineBounds", propertyL);
        }
        // TBD: register container listener to update frame listeners on adding/removing
        // TBD: componentListener on desktop that handles maximizing frame
        //   (JW: didn't really understand what that one is doing in the original)
    }


    @Override
    public Dimension getPreferredSize(JComponent c) {
        JDesktopPane dt = getDesktopPane(c);
        Dimension prefSize = super.getPreferredSize(c);
        //System.out.println("prefSize: " + prefSize);
        // inititialize the max to the first normalized bounds
        Rectangle max = dt.getAllFrames()[0].getNormalBounds();
        for (JInternalFrame jif : dt.getAllFrames()) {
            max.add(jif.getNormalBounds());
            Rectangle outline = (Rectangle) jif
                    .getClientProperty("outlineBounds");
            if (outline != null) {
                max.add(outline);
            }
        }
        // TBD: cope with frames at negative locations
        //System.out.println("maxBounds(): " + max);
        int x1 = max.width + (max.x * 2) < prefSize.width ? prefSize.width
                : max.width + (max.x * 2);
        int y1 = max.height + (max.y * 2) < prefSize.height ? prefSize.height
                : max.height + (max.y * 2);
        //System.out.println("x,y: " + x1 + "," + y1);
        return new Dimension(x1, y1);
    }


    @Override
    public void paint(Graphics g, JComponent c) {
        super.paint(g, c);
        JDesktopPane desktop = getDesktopPane(c);
        JInternalFrame selected = desktop.getSelectedFrame();
        if (selected == null) return;
        Rectangle outline = (Rectangle) selected.getClientProperty("outlineBounds");
        if (outline == null) return;
        Rectangle bounds = outline; //SwingUtilities.convertRectangle(, outline, this);
        g.drawRect(bounds.x, bounds.y, bounds.width, bounds.height);
    }

    protected JDesktopPane getDesktopPane(JComponent c) {
        JDesktopPane desktop = ((JLayer<JDesktopPane>) c).getView();
        return desktop;
    }

    public static class MyDesktopManager extends DefaultDesktopManager {
        private Point currentLoc;

        @Override
        public void dragFrame(JComponent f, int newX, int newY) {
            if (isOutline(f)) {
                // take over the outline drawing
                currentLoc = new Point(newX, newY);
                Rectangle bounds = new Rectangle(currentLoc, f.getSize());
                f.putClientProperty("outlineBounds", bounds);
            } else {
                // call super only if not outline
                // handle outline drawing ourselves
                super.dragFrame(f, newX, newY);
            }
        }

        @Override
        public void beginDraggingFrame(JComponent f) {
            super.beginDraggingFrame(f);
            if (isOutline(f)) {
                currentLoc = f.getLocation();
                f.putClientProperty("outlineBounds", f.getBounds());
            }
        }

        @Override
        public void endDraggingFrame(JComponent f) {
            if (isOutline(f) && currentLoc != null) {
                setBoundsForFrame(f, currentLoc.x, currentLoc.y, f.getWidth(), f.getHeight() );
                f.putClientProperty("outlineBounds", null);
            } else {
                super.endDraggingFrame(f);
            }
        }

        protected boolean isOutline(JComponent f) {
            return ((JInternalFrame) f).getDesktopPane().getDragMode() == 
                JDesktopPane.OUTLINE_DRAG_MODE;
        }
    }

}

usage:

JDesktopPane dt = new JDesktopPane(); 
// add internalframes
...
// decorate the pane with the layer
JLayer<JDesktopPane> layer = new JLayer<>(dt, new DesktopLayerUI());
gui.add(new JScrollPane(layer), BorderLayout.CENTER);

There's a slight catch (read: didn't yet figure out how to fix it): JLayer implements Scrollable - its implementation returns false for tracksXX (if the decorated component isn't a Scrollable itself - JDesktopPane isn't), meaning that the desktop inside a scrollPane is always sized to its prefSize which shows the grayish viewport at the trailing/bottom area if the scrollPane is larger.

1
camickr On

You should be able to use the Drag Layout to handle the resizing of the desktop pane as components are dragged.