/*
* $Id: MultiSplitLayout.java,v 1.15 2005/10/26 14:29:54 hansmuller Exp $
*
* Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
* Santa Clara, California 95054, U.S.A. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.openstreetmap.josm.gui.widgets;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Rectangle;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import javax.swing.UIManager;
import org.openstreetmap.josm.tools.CheckParameterUtil;
/**
* The MultiSplitLayout layout manager recursively arranges its
* components in row and column groups called "Splits". Elements of
* the layout are separated by gaps called "Dividers". The overall
* layout is defined with a simple tree model whose nodes are
* instances of MultiSplitLayout.Split, MultiSplitLayout.Divider,
* and MultiSplitLayout.Leaf. Named Leaf nodes represent the space
* allocated to a component that was added with a constraint that
* matches the Leaf's name. Extra space is distributed
* among row/column siblings according to their 0.0 to 1.0 weight.
* If no weights are specified then the last sibling always gets
* all of the extra space, or space reduction.
*
* <p>
* Although MultiSplitLayout can be used with any Container, it's
* the default layout manager for MultiSplitPane. MultiSplitPane
* supports interactively dragging the Dividers, accessibility,
* and other features associated with split panes.
*
* <p>
* All properties in this class are bound: when a properties value
* is changed, all PropertyChangeListeners are fired.
*
* @author Hans Muller - SwingX
* @see MultiSplitPane
*/
public class MultiSplitLayout implements LayoutManager {
private final Map<String, Component> childMap = new HashMap<>();
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
private Node model;
private int dividerSize;
private boolean floatingDividers = true;
/**
* Create a MultiSplitLayout with a default model with a single
* Leaf node named "default".
*
* #see setModel
*/
public MultiSplitLayout() {
this(new Leaf("default"));
}
/**
* Create a MultiSplitLayout with the specified model.
*
* #see setModel
* @param model model
*/
public MultiSplitLayout(Node model) {
this.model = model;
this.dividerSize = UIManager.getInt("SplitPane.dividerSize");
if (this.dividerSize == 0) {
this.dividerSize = 7;
}
}
/**
* Add property change listener.
* @param listener listener to add
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
if (listener != null) {
pcs.addPropertyChangeListener(listener);
}
}
/**
* Remove property change listener.
* @param listener listener to remove
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
if (listener != null) {
pcs.removePropertyChangeListener(listener);
}
}
/**
* Replies list of property change listeners.
* @return list of property change listeners
*/
public PropertyChangeListener[] getPropertyChangeListeners() {
return pcs.getPropertyChangeListeners();
}
private void firePCS(String propertyName, Object oldValue, Object newValue) {
if (!(oldValue != null && newValue != null && oldValue.equals(newValue))) {
pcs.firePropertyChange(propertyName, oldValue, newValue);
}
}
/**
* Return the root of the tree of Split, Leaf, and Divider nodes
* that define this layout.
*
* @return the value of the model property
* @see #setModel
*/
public Node getModel() {
return model;
}
/**
* Set the root of the tree of Split, Leaf, and Divider nodes
* that define this layout. The model can be a Split node
* (the typical case) or a Leaf. The default value of this
* property is a Leaf named "default".
*
* @param model the root of the tree of Split, Leaf, and Divider node
* @throws IllegalArgumentException if model is a Divider or null
* @see #getModel
*/
public void setModel(Node model) {
if ((model == null) || (model instanceof Divider))
throw new IllegalArgumentException("invalid model");
Node oldModel = model;
this.model = model;
firePCS("model", oldModel, model);
}
/**
* Returns the width of Dividers in Split rows, and the height of
* Dividers in Split columns.
*
* @return the value of the dividerSize property
* @see #setDividerSize
*/
public int getDividerSize() {
return dividerSize;
}
/**
* Sets the width of Dividers in Split rows, and the height of
* Dividers in Split columns. The default value of this property
* is the same as for JSplitPane Dividers.
*
* @param dividerSize the size of dividers (pixels)
* @throws IllegalArgumentException if dividerSize < 0
* @see #getDividerSize
*/
public void setDividerSize(int dividerSize) {
if (dividerSize < 0)
throw new IllegalArgumentException("invalid dividerSize");
int oldDividerSize = this.dividerSize;
this.dividerSize = dividerSize;
firePCS("dividerSize", oldDividerSize, dividerSize);
}
/**
* @return the value of the floatingDividers property
* @see #setFloatingDividers
*/
public boolean getFloatingDividers() {
return floatingDividers;
}
/**
* If true, Leaf node bounds match the corresponding component's
* preferred size and Splits/Dividers are resized accordingly.
* If false then the Dividers define the bounds of the adjacent
* Split and Leaf nodes. Typically this property is set to false
* after the (MultiSplitPane) user has dragged a Divider.
* @param floatingDividers boolean value
*
* @see #getFloatingDividers
*/
public void setFloatingDividers(boolean floatingDividers) {
boolean oldFloatingDividers = this.floatingDividers;
this.floatingDividers = floatingDividers;
firePCS("floatingDividers", oldFloatingDividers, floatingDividers);
}
/**
* Add a component to this MultiSplitLayout. The
* <code>name</code> should match the name property of the Leaf
* node that represents the bounds of <code>child</code>. After
* layoutContainer() recomputes the bounds of all of the nodes in
* the model, it will set this child's bounds to the bounds of the
* Leaf node with <code>name</code>. Note: if a component was already
* added with the same name, this method does not remove it from
* its parent.
*
* @param name identifies the Leaf node that defines the child's bounds
* @param child the component to be added
* @see #removeLayoutComponent
*/
@Override
public void addLayoutComponent(String name, Component child) {
if (name == null)
throw new IllegalArgumentException("name not specified");
childMap.put(name, child);
}
/**
* Removes the specified component from the layout.
*
* @param child the component to be removed
* @see #addLayoutComponent
*/
@Override
public void removeLayoutComponent(Component child) {
String name = child.getName();
if (name != null) {
childMap.remove(name);
}
}
private Component childForNode(Node node) {
if (node instanceof Leaf) {
Leaf leaf = (Leaf) node;
String name = leaf.getName();
return (name != null) ? childMap.get(name) : null;
}
return null;
}
private Dimension preferredComponentSize(Node node) {
Component child = childForNode(node);
return (child != null) ? child.getPreferredSize() : new Dimension(0, 0);
}
private Dimension preferredNodeSize(Node root) {
if (root instanceof Leaf)
return preferredComponentSize(root);
else if (root instanceof Divider) {
int dividerSize = getDividerSize();
return new Dimension(dividerSize, dividerSize);
} else {
Split split = (Split) root;
List<Node> splitChildren = split.getChildren();
int width = 0;
int height = 0;
if (split.isRowLayout()) {
for (Node splitChild : splitChildren) {
Dimension size = preferredNodeSize(splitChild);
width += size.width;
height = Math.max(height, size.height);
}
} else {
for (Node splitChild : splitChildren) {
Dimension size = preferredNodeSize(splitChild);
width = Math.max(width, size.width);
height += size.height;
}
}
return new Dimension(width, height);
}
}
private Dimension minimumNodeSize(Node root) {
if (root instanceof Leaf) {
Component child = childForNode(root);
return (child != null) ? child.getMinimumSize() : new Dimension(0, 0);
} else if (root instanceof Divider) {
int dividerSize = getDividerSize();
return new Dimension(dividerSize, dividerSize);
} else {
Split split = (Split) root;
List<Node> splitChildren = split.getChildren();
int width = 0;
int height = 0;
if (split.isRowLayout()) {
for (Node splitChild : splitChildren) {
Dimension size = minimumNodeSize(splitChild);
width += size.width;
height = Math.max(height, size.height);
}
} else {
for (Node splitChild : splitChildren) {
Dimension size = minimumNodeSize(splitChild);
width = Math.max(width, size.width);
height += size.height;
}
}
return new Dimension(width, height);
}
}
private static Dimension sizeWithInsets(Container parent, Dimension size) {
Insets insets = parent.getInsets();
int width = size.width + insets.left + insets.right;
int height = size.height + insets.top + insets.bottom;
return new Dimension(width, height);
}
@Override
public Dimension preferredLayoutSize(Container parent) {
Dimension size = preferredNodeSize(getModel());
return sizeWithInsets(parent, size);
}
@Override
public Dimension minimumLayoutSize(Container parent) {
Dimension size = minimumNodeSize(getModel());
return sizeWithInsets(parent, size);
}
private static Rectangle boundsWithYandHeight(Rectangle bounds, double y, double height) {
Rectangle r = new Rectangle();
r.setBounds((int) (bounds.getX()), (int) y, (int) (bounds.getWidth()), (int) height);
return r;
}
private static Rectangle boundsWithXandWidth(Rectangle bounds, double x, double width) {
Rectangle r = new Rectangle();
r.setBounds((int) x, (int) (bounds.getY()), (int) width, (int) (bounds.getHeight()));
return r;
}
private static void minimizeSplitBounds(Split split, Rectangle bounds) {
Rectangle splitBounds = new Rectangle(bounds.x, bounds.y, 0, 0);
List<Node> splitChildren = split.getChildren();
Node lastChild = splitChildren.get(splitChildren.size() - 1);
Rectangle lastChildBounds = lastChild.getBounds();
if (split.isRowLayout()) {
int lastChildMaxX = lastChildBounds.x + lastChildBounds.width;
splitBounds.add(lastChildMaxX, bounds.y + bounds.height);
} else {
int lastChildMaxY = lastChildBounds.y + lastChildBounds.height;
splitBounds.add(bounds.x + bounds.width, lastChildMaxY);
}
split.setBounds(splitBounds);
}
private void layoutShrink(Split split, Rectangle bounds) {
Rectangle splitBounds = split.getBounds();
ListIterator<Node> splitChildren = split.getChildren().listIterator();
if (split.isRowLayout()) {
int totalWidth = 0; // sum of the children's widths
int minWeightedWidth = 0; // sum of the weighted childrens' min widths
int totalWeightedWidth = 0; // sum of the weighted childrens' widths
for (Node splitChild : split.getChildren()) {
int nodeWidth = splitChild.getBounds().width;
int nodeMinWidth = Math.min(nodeWidth, minimumNodeSize(splitChild).width);
totalWidth += nodeWidth;
if (splitChild.getWeight() > 0.0) {
minWeightedWidth += nodeMinWidth;
totalWeightedWidth += nodeWidth;
}
}
double x = bounds.getX();
double extraWidth = splitBounds.getWidth() - bounds.getWidth();
double availableWidth = extraWidth;
boolean onlyShrinkWeightedComponents =
(totalWeightedWidth - minWeightedWidth) > extraWidth;
while (splitChildren.hasNext()) {
Node splitChild = splitChildren.next();
Rectangle splitChildBounds = splitChild.getBounds();
double minSplitChildWidth = minimumNodeSize(splitChild).getWidth();
double splitChildWeight = onlyShrinkWeightedComponents
? splitChild.getWeight()
: (splitChildBounds.getWidth() / totalWidth);
if (!splitChildren.hasNext()) {
double newWidth = Math.max(minSplitChildWidth, bounds.getMaxX() - x);
Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
layout2(splitChild, newSplitChildBounds);
} else if ((availableWidth > 0.0) && (splitChildWeight > 0.0)) {
double allocatedWidth = Math.rint(splitChildWeight * extraWidth);
double oldWidth = splitChildBounds.getWidth();
double newWidth = Math.max(minSplitChildWidth, oldWidth - allocatedWidth);
Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
layout2(splitChild, newSplitChildBounds);
availableWidth -= (oldWidth - splitChild.getBounds().getWidth());
} else {
double existingWidth = splitChildBounds.getWidth();
Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, existingWidth);
layout2(splitChild, newSplitChildBounds);
}
x = splitChild.getBounds().getMaxX();
}
} else {
int totalHeight = 0; // sum of the children's heights
int minWeightedHeight = 0; // sum of the weighted childrens' min heights
int totalWeightedHeight = 0; // sum of the weighted childrens' heights
for (Node splitChild : split.getChildren()) {
int nodeHeight = splitChild.getBounds().height;
int nodeMinHeight = Math.min(nodeHeight, minimumNodeSize(splitChild).height);
totalHeight += nodeHeight;
if (splitChild.getWeight() > 0.0) {
minWeightedHeight += nodeMinHeight;
totalWeightedHeight += nodeHeight;
}
}
double y = bounds.getY();
double extraHeight = splitBounds.getHeight() - bounds.getHeight();
double availableHeight = extraHeight;
boolean onlyShrinkWeightedComponents =
(totalWeightedHeight - minWeightedHeight) > extraHeight;
while (splitChildren.hasNext()) {
Node splitChild = splitChildren.next();
Rectangle splitChildBounds = splitChild.getBounds();
double minSplitChildHeight = minimumNodeSize(splitChild).getHeight();
double splitChildWeight = onlyShrinkWeightedComponents
? splitChild.getWeight()
: (splitChildBounds.getHeight() / totalHeight);
if (!splitChildren.hasNext()) {
double oldHeight = splitChildBounds.getHeight();
double newHeight = Math.max(minSplitChildHeight, bounds.getMaxY() - y);
Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
layout2(splitChild, newSplitChildBounds);
availableHeight -= (oldHeight - splitChild.getBounds().getHeight());
} else if ((availableHeight > 0.0) && (splitChildWeight > 0.0)) {
double allocatedHeight = Math.rint(splitChildWeight * extraHeight);
double oldHeight = splitChildBounds.getHeight();
double newHeight = Math.max(minSplitChildHeight, oldHeight - allocatedHeight);
Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
layout2(splitChild, newSplitChildBounds);
availableHeight -= (oldHeight - splitChild.getBounds().getHeight());
} else {
double existingHeight = splitChildBounds.getHeight();
Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, existingHeight);
layout2(splitChild, newSplitChildBounds);
}
y = splitChild.getBounds().getMaxY();
}
}
/* The bounds of the Split node root are set to be
* big enough to contain all of its children. Since
* Leaf children can't be reduced below their
* (corresponding java.awt.Component) minimum sizes,
* the size of the Split's bounds maybe be larger than
* the bounds we were asked to fit within.
*/
minimizeSplitBounds(split, bounds);
}
private void layoutGrow(Split split, Rectangle bounds) {
Rectangle splitBounds = split.getBounds();
ListIterator<Node> splitChildren = split.getChildren().listIterator();
Node lastWeightedChild = split.lastWeightedChild();
if (split.isRowLayout()) {
/* Layout the Split's child Nodes' along the X axis. The bounds
* of each child will have the same y coordinate and height as the
* layoutGrow() bounds argument. Extra width is allocated to the
* to each child with a non-zero weight:
* newWidth = currentWidth + (extraWidth * splitChild.getWeight())
* Any extraWidth "left over" (that's availableWidth in the loop
* below) is given to the last child. Note that Dividers always
* have a weight of zero, and they're never the last child.
*/
double x = bounds.getX();
double extraWidth = bounds.getWidth() - splitBounds.getWidth();
double availableWidth = extraWidth;
while (splitChildren.hasNext()) {
Node splitChild = splitChildren.next();
Rectangle splitChildBounds = splitChild.getBounds();
double splitChildWeight = splitChild.getWeight();
if (!splitChildren.hasNext()) {
double newWidth = bounds.getMaxX() - x;
Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
layout2(splitChild, newSplitChildBounds);
} else if ((availableWidth > 0.0) && (splitChildWeight > 0.0)) {
double allocatedWidth = splitChild.equals(lastWeightedChild)
? availableWidth
: Math.rint(splitChildWeight * extraWidth);
double newWidth = splitChildBounds.getWidth() + allocatedWidth;
Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
layout2(splitChild, newSplitChildBounds);
availableWidth -= allocatedWidth;
} else {
double existingWidth = splitChildBounds.getWidth();
Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, existingWidth);
layout2(splitChild, newSplitChildBounds);
}
x = splitChild.getBounds().getMaxX();
}
} else {
/* Layout the Split's child Nodes' along the Y axis. The bounds
* of each child will have the same x coordinate and width as the
* layoutGrow() bounds argument. Extra height is allocated to the
* to each child with a non-zero weight:
* newHeight = currentHeight + (extraHeight * splitChild.getWeight())
* Any extraHeight "left over" (that's availableHeight in the loop
* below) is given to the last child. Note that Dividers always
* have a weight of zero, and they're never the last child.
*/
double y = bounds.getY();
double extraHeight = bounds.getMaxY() - splitBounds.getHeight();
double availableHeight = extraHeight;
while (splitChildren.hasNext()) {
Node splitChild = splitChildren.next();
Rectangle splitChildBounds = splitChild.getBounds();
double splitChildWeight = splitChild.getWeight();
if (!splitChildren.hasNext()) {
double newHeight = bounds.getMaxY() - y;
Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
layout2(splitChild, newSplitChildBounds);
} else if ((availableHeight > 0.0) && (splitChildWeight > 0.0)) {
double allocatedHeight = splitChild.equals(lastWeightedChild)
? availableHeight
: Math.rint(splitChildWeight * extraHeight);
double newHeight = splitChildBounds.getHeight() + allocatedHeight;
Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
layout2(splitChild, newSplitChildBounds);
availableHeight -= allocatedHeight;
} else {
double existingHeight = splitChildBounds.getHeight();
Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, existingHeight);
layout2(splitChild, newSplitChildBounds);
}
y = splitChild.getBounds().getMaxY();
}
}
}
/* Second pass of the layout algorithm: branch to layoutGrow/Shrink
* as needed.
*/
private void layout2(Node root, Rectangle bounds) {
if (root instanceof Leaf) {
Component child = childForNode(root);
if (child != null) {
child.setBounds(bounds);
}
root.setBounds(bounds);
} else if (root instanceof Divider) {
root.setBounds(bounds);
} else if (root instanceof Split) {
Split split = (Split) root;
boolean grow = split.isRowLayout()
? split.getBounds().width <= bounds.width
: (split.getBounds().height <= bounds.height);
if (grow) {
layoutGrow(split, bounds);
root.setBounds(bounds);
} else {
layoutShrink(split, bounds);
// split.setBounds() called in layoutShrink()
}
}
}
/* First pass of the layout algorithm.
*
* If the Dividers are "floating" then set the bounds of each
* node to accomodate the preferred size of all of the
* Leaf's java.awt.Components. Otherwise, just set the bounds
* of each Leaf/Split node so that it's to the left of (for
* Split.isRowLayout() Split children) or directly above
* the Divider that follows.
*
* This pass sets the bounds of each Node in the layout model. It
* does not resize any of the parent Container's
* (java.awt.Component) children. That's done in the second pass,
* see layoutGrow() and layoutShrink().
*/
private void layout1(Node root, Rectangle bounds) {
if (root instanceof Leaf) {
root.setBounds(bounds);
} else if (root instanceof Split) {
Split split = (Split) root;
Iterator<Node> splitChildren = split.getChildren().iterator();
Rectangle childBounds;
int dividerSize = getDividerSize();
/* Layout the Split's child Nodes' along the X axis. The bounds
* of each child will have the same y coordinate and height as the
* layout1() bounds argument.
*
* Note: the column layout code - that's the "else" clause below
* this if, is identical to the X axis (rowLayout) code below.
*/
if (split.isRowLayout()) {
double x = bounds.getX();
while (splitChildren.hasNext()) {
Node splitChild = splitChildren.next();
Divider dividerChild =
splitChildren.hasNext() ? (Divider) (splitChildren.next()) : null;
double childWidth;
if (getFloatingDividers()) {
childWidth = preferredNodeSize(splitChild).getWidth();
} else {
if (dividerChild != null) {
childWidth = dividerChild.getBounds().getX() - x;
} else {
childWidth = split.getBounds().getMaxX() - x;
}
}
childBounds = boundsWithXandWidth(bounds, x, childWidth);
layout1(splitChild, childBounds);
if (getFloatingDividers() && (dividerChild != null)) {
double dividerX = childBounds.getMaxX();
Rectangle dividerBounds = boundsWithXandWidth(bounds, dividerX, dividerSize);
dividerChild.setBounds(dividerBounds);
}
if (dividerChild != null) {
x = dividerChild.getBounds().getMaxX();
}
}
} else {
/* Layout the Split's child Nodes' along the Y axis. The bounds
* of each child will have the same x coordinate and width as the
* layout1() bounds argument. The algorithm is identical to what's
* explained above, for the X axis case.
*/
double y = bounds.getY();
while (splitChildren.hasNext()) {
Node splitChild = splitChildren.next();
Divider dividerChild =
splitChildren.hasNext() ? (Divider) splitChildren.next() : null;
double childHeight;
if (getFloatingDividers()) {
childHeight = preferredNodeSize(splitChild).getHeight();
} else {
if (dividerChild != null) {
childHeight = dividerChild.getBounds().getY() - y;
} else {
childHeight = split.getBounds().getMaxY() - y;
}
}
childBounds = boundsWithYandHeight(bounds, y, childHeight);
layout1(splitChild, childBounds);
if (getFloatingDividers() && (dividerChild != null)) {
double dividerY = childBounds.getMaxY();
Rectangle dividerBounds = boundsWithYandHeight(bounds, dividerY, dividerSize);
dividerChild.setBounds(dividerBounds);
}
if (dividerChild != null) {
y = dividerChild.getBounds().getMaxY();
}
}
}
/* The bounds of the Split node root are set to be just
* big enough to contain all of its children, but only
* along the axis it's allocating space on. That's
* X for rows, Y for columns. The second pass of the
* layout algorithm - see layoutShrink()/layoutGrow()
* allocates extra space.
*/
minimizeSplitBounds(split, bounds);
}
}
/**
* The specified Node is either the wrong type or was configured incorrectly.
*/
public static class InvalidLayoutException extends RuntimeException {
private final transient Node node;
/**
* Constructs a new {@code InvalidLayoutException}.
* @param msg the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
* @param node node
*/
public InvalidLayoutException(String msg, Node node) {
super(msg);
this.node = node;
}
/**
* @return the invalid Node.
*/
public Node getNode() {
return node;
}
}
private static void throwInvalidLayout(String msg, Node node) {
throw new InvalidLayoutException(msg, node);
}
private static void checkLayout(Node root) {
if (root instanceof Split) {
Split split = (Split) root;
if (split.getChildren().size() <= 2) {
throwInvalidLayout("Split must have > 2 children", root);
}
Iterator<Node> splitChildren = split.getChildren().iterator();
double weight = 0.0;
while (splitChildren.hasNext()) {
Node splitChild = splitChildren.next();
if (splitChild instanceof Divider) {
throwInvalidLayout("expected a Split or Leaf Node", splitChild);
}
if (splitChildren.hasNext()) {
Node dividerChild = splitChildren.next();
if (!(dividerChild instanceof Divider)) {
throwInvalidLayout("expected a Divider Node", dividerChild);
}
}
weight += splitChild.getWeight();
checkLayout(splitChild);
}
if (weight > 1.0 + 0.000000001) { /* add some epsilon to a double check */
throwInvalidLayout("Split children's total weight > 1.0", root);
}
}
}
/**
* Compute the bounds of all of the Split/Divider/Leaf Nodes in
* the layout model, and then set the bounds of each child component
* with a matching Leaf Node.
*/
@Override
public void layoutContainer(Container parent) {
checkLayout(getModel());
Insets insets = parent.getInsets();
Dimension size = parent.getSize();
int width = size.width - (insets.left + insets.right);
int height = size.height - (insets.top + insets.bottom);
Rectangle bounds = new Rectangle(insets.left, insets.top, width, height);
layout1(getModel(), bounds);
layout2(getModel(), bounds);
}
private static Divider dividerAt(Node root, int x, int y) {
if (root instanceof Divider) {
Divider divider = (Divider) root;
return divider.getBounds().contains(x, y) ? divider : null;
} else if (root instanceof Split) {
Split split = (Split) root;
for (Node child : split.getChildren()) {
if (child.getBounds().contains(x, y))
return dividerAt(child, x, y);
}
}
return null;
}
/**
* Return the Divider whose bounds contain the specified
* point, or null if there isn't one.
*
* @param x x coordinate
* @param y y coordinate
* @return the Divider at x,y
*/
public Divider dividerAt(int x, int y) {
return dividerAt(getModel(), x, y);
}
private static boolean nodeOverlapsRectangle(Node node, Rectangle r2) {
Rectangle r1 = node.getBounds();
return
(r1.x <= (r2.x + r2.width)) && ((r1.x + r1.width) >= r2.x) &&
(r1.y <= (r2.y + r2.height)) && ((r1.y + r1.height) >= r2.y);
}
private static List<Divider> dividersThatOverlap(Node root, Rectangle r) {
if (nodeOverlapsRectangle(root, r) && (root instanceof Split)) {
List<Divider> dividers = new ArrayList<>();
for (Node child : ((Split) root).getChildren()) {
if (child instanceof Divider) {
if (nodeOverlapsRectangle(child, r)) {
dividers.add((Divider) child);
}
} else if (child instanceof Split) {
dividers.addAll(dividersThatOverlap(child, r));
}
}
return dividers;
} else
return Collections.emptyList();
}
/**
* Return the Dividers whose bounds overlap the specified
* Rectangle.
*
* @param r target Rectangle
* @return the Dividers that overlap r
* @throws IllegalArgumentException if the Rectangle is null
*/
public List<Divider> dividersThatOverlap(Rectangle r) {
CheckParameterUtil.ensureParameterNotNull(r, "r");
return dividersThatOverlap(getModel(), r);
}
/**
* Base class for the nodes that model a MultiSplitLayout.
*/
public abstract static class Node {
private Split parent;
private Rectangle bounds = new Rectangle();
private double weight;
/**
* Returns the Split parent of this Node, or null.
*
* This method isn't called getParent(), in order to avoid problems
* with recursive object creation when using XmlDecoder.
*
* @return the value of the parent property.
* @see #setParent
*/
public Split getParent() {
return parent;
}
/**
* Set the value of this Node's parent property. The default
* value of this property is null.
*
* This method isn't called setParent(), in order to avoid problems
* with recursive object creation when using XmlEncoder.
*
* @param parent a Split or null
* @see #getParent
*/
public void setParent(Split parent) {
this.parent = parent;
}
/**
* Returns the bounding Rectangle for this Node.
*
* @return the value of the bounds property.
* @see #setBounds
*/
public Rectangle getBounds() {
return new Rectangle(this.bounds);
}
/**
* Set the bounding Rectangle for this node. The value of
* bounds may not be null. The default value of bounds
* is equal to <code>new Rectangle(0,0,0,0)</code>.
*
* @param bounds the new value of the bounds property
* @throws IllegalArgumentException if bounds is null
* @see #getBounds
*/
public void setBounds(Rectangle bounds) {
CheckParameterUtil.ensureParameterNotNull(bounds, "bounds");
this.bounds = new Rectangle(bounds);
}
/**
* Value between 0.0 and 1.0 used to compute how much space
* to add to this sibling when the layout grows or how
* much to reduce when the layout shrinks.
*
* @return the value of the weight property
* @see #setWeight
*/
public double getWeight() {
return weight;
}
/**
* The weight property is a between 0.0 and 1.0 used to
* compute how much space to add to this sibling when the
* layout grows or how much to reduce when the layout shrinks.
* If rowLayout is true then this node's width grows
* or shrinks by (extraSpace * weight). If rowLayout is false,
* then the node's height is changed. The default value
* of weight is 0.0.
*
* @param weight a double between 0.0 and 1.0
* @throws IllegalArgumentException if weight is not between 0.0 and 1.0
* @see #getWeight
* @see MultiSplitLayout#layoutContainer
*/
public void setWeight(double weight) {
if ((weight < 0.0) || (weight > 1.0))
throw new IllegalArgumentException("invalid weight");
this.weight = weight;
}
private Node siblingAtOffset(int offset) {
Split parent = getParent();
if (parent == null)
return null;
List<Node> siblings = parent.getChildren();
int index = siblings.indexOf(this);
if (index == -1)
return null;
index += offset;
return ((index > -1) && (index < siblings.size())) ? siblings.get(index) : null;
}
/**
* Return the Node that comes after this one in the parent's
* list of children, or null. If this node's parent is null,
* or if it's the last child, then return null.
*
* @return the Node that comes after this one in the parent's list of children.
* @see #previousSibling
* @see #getParent
*/
public Node nextSibling() {
return siblingAtOffset(+1);
}
/**
* Return the Node that comes before this one in the parent's
* list of children, or null. If this node's parent is null,
* or if it's the last child, then return null.
*
* @return the Node that comes before this one in the parent's list of children.
* @see #nextSibling
* @see #getParent
*/
public Node previousSibling() {
return siblingAtOffset(-1);
}
}
/**
* Defines a vertical or horizontal subdivision into two or more
* tiles.
*/
public static class Split extends Node {
private List<Node> children = Collections.emptyList();
private boolean rowLayout = true;
/**
* Returns true if the this Split's children are to be
* laid out in a row: all the same height, left edge
* equal to the previous Node's right edge. If false,
* children are laid on in a column.
*
* @return the value of the rowLayout property.
* @see #setRowLayout
*/
public boolean isRowLayout() {
return rowLayout;
}
/**
* Set the rowLayout property. If true, all of this Split's
* children are to be laid out in a row: all the same height,
* each node's left edge equal to the previous Node's right
* edge. If false, children are laid on in a column. Default value is true.
*
* @param rowLayout true for horizontal row layout, false for column
* @see #isRowLayout
*/
public void setRowLayout(boolean rowLayout) {
this.rowLayout = rowLayout;
}
/**
* Returns this Split node's children. The returned value
* is not a reference to the Split's internal list of children
*
* @return the value of the children property.
* @see #setChildren
*/
public List<Node> getChildren() {
return new ArrayList<>(children);
}
/**
* Set's the children property of this Split node. The parent
* of each new child is set to this Split node, and the parent
* of each old child (if any) is set to null. This method
* defensively copies the incoming List. Default value is an empty List.
*
* @param children List of children
* @throws IllegalArgumentException if children is null
* @see #getChildren
*/
public void setChildren(List<Node> children) {
if (children == null)
throw new IllegalArgumentException("children must be a non-null List");
for (Node child : this.children) {
child.setParent(null);
}
this.children = new ArrayList<>(children);
for (Node child : this.children) {
child.setParent(this);
}
}
/**
* Convenience method that returns the last child whose weight
* is > 0.0.
*
* @return the last child whose weight is > 0.0.
* @see #getChildren
* @see Node#getWeight
*/
public final Node lastWeightedChild() {
List<Node> children = getChildren();
Node weightedChild = null;
for (Node child : children) {
if (child.getWeight() > 0.0) {
weightedChild = child;
}
}
return weightedChild;
}
@Override
public String toString() {
int nChildren = getChildren().size();
StringBuilder sb = new StringBuilder("MultiSplitLayout.Split");
sb.append(isRowLayout() ? " ROW [" : " COLUMN [")
.append(nChildren + ((nChildren == 1) ? " child" : " children"))
.append("] ")
.append(getBounds());
return sb.toString();
}
}
/**
* Models a java.awt Component child.
*/
public static class Leaf extends Node {
private String name = "";
/**
* Create a Leaf node. The default value of name is "".
*/
public Leaf() {
// Name can be set later with setName()
}
/**
* Create a Leaf node with the specified name. Name can not be null.
*
* @param name value of the Leaf's name property
* @throws IllegalArgumentException if name is null
*/
public Leaf(String name) {
CheckParameterUtil.ensureParameterNotNull(name, "name");
this.name = name;
}
/**
* Return the Leaf's name.
*
* @return the value of the name property.
* @see #setName
*/
public String getName() {
return name;
}
/**
* Set the value of the name property. Name may not be null.
*
* @param name value of the name property
* @throws IllegalArgumentException if name is null
*/
public void setName(String name) {
CheckParameterUtil.ensureParameterNotNull(name, "name");
this.name = name;
}
@Override
public String toString() {
return new StringBuilder("MultiSplitLayout.Leaf \"")
.append(getName())
.append("\" weight=")
.append(getWeight())
.append(' ')
.append(getBounds())
.toString();
}
}
/**
* Models a single vertical/horiztonal divider.
*/
public static class Divider extends Node {
/**
* Convenience method, returns true if the Divider's parent
* is a Split row (a Split with isRowLayout() true), false
* otherwise. In other words if this Divider's major axis
* is vertical, return true.
*
* @return true if this Divider is part of a Split row.
*/
public final boolean isVertical() {
Split parent = getParent();
return parent != null && parent.isRowLayout();
}
/**
* Dividers can't have a weight, they don't grow or shrink.
* @throws UnsupportedOperationException always
*/
@Override
public void setWeight(double weight) {
throw new UnsupportedOperationException();
}
@Override
public String toString() {
return "MultiSplitLayout.Divider " + getBounds();
}
}
}