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.