/* * $Id: MultiSplitLayout.java 3957 2011-03-15 18:27:26Z kschaefe $ * * 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.openflexo.swing.layout; 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.io.IOException; import java.io.Reader; import java.io.Serializable; import java.io.StreamTokenizer; import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; 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.openflexo.toolbox.StringUtils; import org.openflexo.xmlcode.XMLSerializable; /** * 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 * @author Luan O'Carroll * @see JXMultiSplitPane */ /* * Changes by Luan O'Carroll * 1 Support for visibility added. */ public class MultiSplitLayout implements LayoutManager, Serializable { public static final int DEFAULT_LAYOUT = 0; public static final int NO_MIN_SIZE_LAYOUT = 1; public static final int USER_MIN_SIZE_LAYOUT = 2; private final Map<String, Component> childMap = new HashMap<String, Component>(); private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); private Node model; private int dividerSize; private boolean floatingDividers = true; private boolean removeDividers = true; private boolean layoutByWeight = false; private int layoutMode; private int userMinSize = 20; /** * 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 a default model with a single Leaf node named "default". * * @param layoutByWeight * if true the layout is initialized in proportion to the node weights rather than the component preferred sizes. #see * setModel */ public MultiSplitLayout(boolean layoutByWeight) { this(new Leaf("default")); this.layoutByWeight = layoutByWeight; } /** * Set the size of the child components to match the weights of the children. If the components to not all specify a weight then the * available layout space is divided equally between the components. */ public void layoutByWeight(Container parent) { doLayoutByWeight(parent); layoutContainer(parent); } /** * Set the size of the child components to match the weights of the children. If the components to not all specify a weight then the * available layout space is divided equally between the components. */ private void doLayoutByWeight(Container parent) { Dimension size = parent.getSize(); Insets insets = parent.getInsets(); 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); if (model instanceof Leaf) { model.setBounds(bounds); } else if (model instanceof Split) { doLayoutByWeight(model, bounds); } } private void doLayoutByWeight(Node node, Rectangle bounds) { int width = bounds.width; int height = bounds.height; Split split = (Split) node; List<Node> splitChildren = split.getChildren(); double distributableWeight = 1.0; int unweightedComponents = 0; int dividerSpace = 0; for (Node splitChild : splitChildren) { if (!splitChild.isVisible()) { continue; } else if (splitChild instanceof Divider) { dividerSpace += dividerSize; continue; } double weight = splitChild.getWeight(); if (weight > 0.0) { distributableWeight -= weight; } else { unweightedComponents++; } } if (split.isRowLayout()) { width -= dividerSpace; double distributableWidth = width * distributableWeight; for (Node splitChild : splitChildren) { if (!splitChild.isVisible() || splitChild instanceof Divider) { continue; } double weight = splitChild.getWeight(); Rectangle splitChildBounds = splitChild.getBounds(); if (weight >= 0) { splitChildBounds = new Rectangle(splitChildBounds.x, splitChildBounds.y, (int) (width * weight), height); } else { splitChildBounds = new Rectangle(splitChildBounds.x, splitChildBounds.y, (int) (distributableWidth / unweightedComponents), height); } if (layoutMode == USER_MIN_SIZE_LAYOUT) { splitChildBounds.setSize(Math.max(splitChildBounds.width, userMinSize), splitChildBounds.height); } splitChild.setBounds(splitChildBounds); if (splitChild instanceof Split) { doLayoutByWeight(splitChild, splitChildBounds); } else { Component comp = getComponentForNode(splitChild); if (comp != null) { comp.setPreferredSize(splitChildBounds.getSize()); } } } } else { height -= dividerSpace; double distributableHeight = height * distributableWeight; for (Node splitChild : splitChildren) { if (!splitChild.isVisible() || splitChild instanceof Divider) { continue; } double weight = splitChild.getWeight(); Rectangle splitChildBounds = splitChild.getBounds(); if (weight >= 0) { splitChildBounds = new Rectangle(splitChildBounds.x, splitChildBounds.y, width, (int) (height * weight)); } else { splitChildBounds = new Rectangle(splitChildBounds.x, splitChildBounds.y, width, (int) (distributableHeight / unweightedComponents)); } if (layoutMode == USER_MIN_SIZE_LAYOUT) { splitChildBounds.setSize(splitChildBounds.width, Math.max(splitChildBounds.height, userMinSize)); } splitChild.setBounds(splitChildBounds); if (splitChild instanceof Split) { doLayoutByWeight(splitChild, splitChildBounds); } else { Component comp = getComponentForNode(splitChild); if (comp != null) { comp.setPreferredSize(splitChildBounds.getSize()); } } } } } /** * Get the component associated with a MultiSplitLayout.Node * * @param n * the layout node * @return the component handled by the layout or null if not found */ public Component getComponentForNode(Node n) { String name = ((Leaf) n).getName(); return name != null ? (Component) childMap.get(name) : null; } /** * Get the MultiSplitLayout.Node associated with a component * * @param comp * the component being positioned by the layout * @return the node associated with the component */ public Node getNodeForComponent(Component comp) { return getNodeForName(getNameForComponent(comp)); } /** * Get the MultiSplitLayout.Node associated with a component * * @param name * the name used to associate a component with the layout * @return the node associated with the component */ public Node getNodeForName(String name) { if (model instanceof Split) { Split split = (Split) model; return getNodeForName(split, name); } else { return null; } } /** * Get the name used to map a component * * @param child * the component * @return the name used to map the component or null if no mapping is found */ public String getNameForComponent(Component child) { String name = null; for (Map.Entry<String, Component> kv : childMap.entrySet()) { if (kv.getValue() == child) { name = kv.getKey(); break; } } return name; } /** * Get the MultiSplitLayout.Node associated with a component * * @param split * the layout split that owns the requested node * @param comp * the component being positioned by the layout * @return the node associated with the component */ public Node getNodeForComponent(Split split, Component comp) { return getNodeForName(split, getNameForComponent(comp)); } /** * Get the MultiSplitLayout.Node associated with a component * * @param split * the layout split that owns the requested node * @param name * the name used to associate a component with the layout * @return the node associated with the component */ public Node getNodeForName(Split split, String name) { for (Node n : split.getChildren()) { if (n instanceof Leaf) { if (((Leaf) n).getName().equals(name)) { return n; } } else if (n instanceof Split) { Node n1 = getNodeForName((Split) n, name); if (n1 != null) { return n1; } } } return null; } /** * Is there a valid model for the layout? * * @return true if there is a model */ public boolean hasModel() { return model != null; } /** * Create a MultiSplitLayout with the specified model. * * #see setModel */ public MultiSplitLayout(Node model) { this.model = model; this.dividerSize = UIManager.getInt("SplitPane.dividerSize"); if (this.dividerSize == 0) { this.dividerSize = 7; } } public void addPropertyChangeListener(PropertyChangeListener listener) { if (listener != null) { pcs.addPropertyChangeListener(listener); } } public void removePropertyChangeListener(PropertyChangeListener listener) { if (listener != null) { pcs.removePropertyChangeListener(listener); } } 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 = getModel(); this.model = model; firePCS("model", oldModel, getModel()); } /** * 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", new Integer(oldDividerSize), new Integer(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. * * @see #getFloatingDividers */ public void setFloatingDividers(boolean floatingDividers) { boolean oldFloatingDividers = this.floatingDividers; this.floatingDividers = floatingDividers; firePCS("floatingDividers", new Boolean(oldFloatingDividers), new Boolean(floatingDividers)); } /** * @return the value of the removeDividers property * @see #setRemoveDividers */ public boolean getRemoveDividers() { return removeDividers; } /** * If true, the next divider is removed when a component is removed from the layout. If false, only the node itself is removed. Normally * the next divider should be removed from the layout when a component is removed. * * @param removeDividers * true to removed the next divider whena component is removed from teh layout */ public void setRemoveDividers(boolean removeDividers) { boolean oldRemoveDividers = this.removeDividers; this.removeDividers = removeDividers; firePCS("removeDividers", new Boolean(oldRemoveDividers), new Boolean(removeDividers)); } /** * 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 = getNameForComponent(child); if (name != null) { childMap.remove(name); } } /** * Removes the specified node from the layout. * * @param name * the name of the component to be removed * @see #addLayoutComponent */ public void removeLayoutNode(String name) { if (name != null) { Node n; if (!(model instanceof Split)) { n = model; } else { n = getNodeForName(name); } childMap.remove(name); if (n != null) { Split s = n.getParent(); s.remove(n); if (removeDividers) { while (s.getChildren().size() < 2) { Split p = s.getParent(); if (p == null) { if (s.getChildren().size() > 0) { model = s.getChildren().get(0); } else { model = null; } return; } if (s.getChildren().size() == 1) { Node next = s.getChildren().get(0); p.replace(s, next); next.setParent(p); } else { p.remove(s); } s = p; } } } else { childMap.remove(name); } } } /** * Show/Hide nodes. Any dividers that are no longer required due to one of the nodes being made visible/invisible are also shown/hidden. * The visibility of the component managed by the node is also changed by this method * * @param name * the node name * @param visible * the new node visible state */ public void displayNode(String name, boolean visible) { Node node = getNodeForName(name); if (node != null) { Component comp = getComponentForNode(node); if (comp != null) { comp.setVisible(visible); } node.setVisible(visible); MultiSplitLayout.Split p = node.getParent(); if (!visible) { p.hide(node); if (!p.isVisible()) { p.getParent().hide(p); } p.checkDividers(p); // If the split has become invisible then the parent may also have a // divider that needs to be hidden. while (!p.isVisible()) { p = p.getParent(); if (p != null) { p.checkDividers(p); } else { break; } } } else { p.restoreDividers(p); } } setFloatingDividers(false); } 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) { if (layoutMode == NO_MIN_SIZE_LAYOUT) { return new Dimension(0, 0); } Component child = childForNode(node); return child != null && child.isVisible() ? child.getPreferredSize() : new Dimension(0, 0); } private Dimension minimumComponentSize(Node node) { if (layoutMode == NO_MIN_SIZE_LAYOUT) { return new Dimension(0, 0); } Component child = childForNode(node); return child != null && child.isVisible() ? child.getMinimumSize() : new Dimension(0, 0); } private Dimension preferredNodeSize(Node root) { if (root instanceof Leaf) { return preferredComponentSize(root); } else if (root instanceof Divider) { if (!((Divider) root).isVisible()) { return new Dimension(0, 0); } int divSize = getDividerSize(); return new Dimension(divSize, divSize); } else { Split split = (Split) root; List<Node> splitChildren = split.getChildren(); int width = 0; int height = 0; if (split.isRowLayout()) { for (Node splitChild : splitChildren) { if (!splitChild.isVisible()) { continue; } Dimension size = preferredNodeSize(splitChild); width += size.width; height = Math.max(height, size.height); } } else { for (Node splitChild : splitChildren) { if (!splitChild.isVisible()) { continue; } Dimension size = preferredNodeSize(splitChild); width = Math.max(width, size.width); height += size.height; } } return new Dimension(width, height); } } /** * Get the minimum size of this node. Sums the minumum sizes of rows or columns to get the overall minimum size for the layout node, * including the dividers. * * @param root * the node whose size is required. * @return the minimum size. */ public Dimension minimumNodeSize(Node root) { assert root.isVisible; if (root instanceof Leaf) { if (layoutMode == NO_MIN_SIZE_LAYOUT) { return new Dimension(0, 0); } Component child = childForNode(root); return child != null && child.isVisible() ? child.getMinimumSize() : new Dimension(0, 0); } else if (root instanceof Divider) { if (!((Divider) root).isVisible()) { return new Dimension(0, 0); } int divSize = getDividerSize(); return new Dimension(divSize, divSize); } else { Split split = (Split) root; List<Node> splitChildren = split.getChildren(); int width = 0; int height = 0; if (split.isRowLayout()) { for (Node splitChild : splitChildren) { if (!splitChild.isVisible()) { continue; } Dimension size = minimumNodeSize(splitChild); width += size.width; height = Math.max(height, size.height); } } else { for (Node splitChild : splitChildren) { if (!splitChild.isVisible()) { continue; } Dimension size = minimumNodeSize(splitChild); width = Math.max(width, size.width); height += size.height; } } return new Dimension(width, height); } } /** * Get the maximum size of this node. Sums the minumum sizes of rows or columns to get the overall maximum size for the layout node, * including the dividers. * * @param root * the node whose size is required. * @return the minimum size. */ public Dimension maximumNodeSize(Node root) { assert root.isVisible; if (root instanceof Leaf) { Component child = childForNode(root); return child != null && child.isVisible() ? child.getMaximumSize() : new Dimension(0, 0); } else if (root instanceof Divider) { if (!((Divider) root).isVisible()) { return new Dimension(0, 0); } int divSize = getDividerSize(); return new Dimension(divSize, divSize); } else { Split split = (Split) root; List<Node> splitChildren = split.getChildren(); int width = Integer.MAX_VALUE; int height = Integer.MAX_VALUE; if (split.isRowLayout()) { for (Node splitChild : splitChildren) { if (!splitChild.isVisible()) { continue; } Dimension size = maximumNodeSize(splitChild); width += size.width; height = Math.min(height, size.height); } } else { for (Node splitChild : splitChildren) { if (!splitChild.isVisible()) { continue; } Dimension size = maximumNodeSize(splitChild); width = Math.min(width, size.width); height += size.height; } } return new Dimension(width, height); } } private 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 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 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 void minimizeSplitBounds(Split split, Rectangle bounds) { assert split.isVisible(); Rectangle splitBounds = new Rectangle(bounds.x, bounds.y, 0, 0); List<Node> splitChildren = split.getChildren(); Node lastChild = null; int lastVisibleChildIdx = splitChildren.size(); do { lastVisibleChildIdx--; lastChild = splitChildren.get(lastVisibleChildIdx); } while (lastVisibleChildIdx > 0 && !lastChild.isVisible()); if (!lastChild.isVisible()) { return; } if (lastVisibleChildIdx >= 0) { 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(); Node lastWeightedChild = split.lastWeightedChild(); 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()) { if (!splitChild.isVisible()) { continue; } int nodeWidth = splitChild.getBounds().width; int nodeMinWidth = 0; if (layoutMode == USER_MIN_SIZE_LAYOUT && !(splitChild instanceof Divider)) { nodeMinWidth = userMinSize; } else if (layoutMode == DEFAULT_LAYOUT) { 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(); if (!splitChild.isVisible()) { if (splitChildren.hasNext()) { splitChildren.next(); } continue; } Rectangle splitChildBounds = splitChild.getBounds(); double minSplitChildWidth = 0.0; if (layoutMode == USER_MIN_SIZE_LAYOUT && !(splitChild instanceof Divider)) { minSplitChildWidth = userMinSize; } else if (layoutMode == DEFAULT_LAYOUT) { 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); } if (splitChild.isVisible()) { if (availableWidth > 0.0 && splitChildWeight > 0.0) { double oldWidth = splitChildBounds.getWidth(); double newWidth; if (splitChild instanceof Divider) { newWidth = dividerSize; } else { double allocatedWidth = Math.rint(splitChildWeight * extraWidth); 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()) { if (!splitChild.isVisible()) { continue; } int nodeHeight = splitChild.getBounds().height; int nodeMinHeight = 0; if (layoutMode == USER_MIN_SIZE_LAYOUT && !(splitChild instanceof Divider)) { nodeMinHeight = userMinSize; } else if (layoutMode == DEFAULT_LAYOUT) { 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(); if (!splitChild.isVisible()) { if (splitChildren.hasNext()) { splitChildren.next(); } continue; } Rectangle splitChildBounds = splitChild.getBounds(); double minSplitChildHeight = 0.0; if (layoutMode == USER_MIN_SIZE_LAYOUT && !(splitChild instanceof Divider)) { minSplitChildHeight = userMinSize; } else if (layoutMode == DEFAULT_LAYOUT) { minSplitChildHeight = minimumNodeSize(splitChild).getHeight(); } double splitChildWeight = onlyShrinkWeightedComponents ? splitChild.getWeight() : splitChildBounds.getHeight() / totalHeight; // If this split child is the last visible node it should all the // remaining space if (!hasMoreVisibleSiblings(splitChild)) { double oldHeight = splitChildBounds.getHeight(); double newHeight; if (splitChild instanceof Divider) { newHeight = dividerSize; } else { newHeight = Math.max(minSplitChildHeight, bounds.getMaxY() - y); } Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight); layout2(splitChild, newSplitChildBounds); availableHeight -= oldHeight - splitChild.getBounds().getHeight(); } else /*if ( splitChild.isVisible()) {*/ if (availableHeight > 0.0 && splitChildWeight > 0.0) { double newHeight; double oldHeight = splitChildBounds.getHeight(); // Prevent the divider from shrinking if (splitChild instanceof Divider) { newHeight = dividerSize; } else { double allocatedHeight = Math.rint(splitChildWeight * extraHeight); 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); } /** * Check if the specified node has any following visible siblings * * @param splitChild * the node to check * @param true if there are visible children following */ private boolean hasMoreVisibleSiblings(Node splitChild) { Node next = splitChild.nextSibling(); if (next == null) { return false; } do { if (next.isVisible()) { return true; } next = next.nextSibling(); } while (next != null); return false; } private void layoutGrow(Split split, Rectangle bounds) { Rectangle splitBounds = split.getBounds(); ListIterator<Node> splitChildren = split.getChildren().listIterator(); Node lastWeightedChild = split.lastWeightedChild(); /* 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. */ if (split.isRowLayout()) { double x = bounds.getX(); double extraWidth = bounds.getWidth() - splitBounds.getWidth(); double availableWidth = extraWidth; while (splitChildren.hasNext()) { Node splitChild = splitChildren.next(); if (!splitChild.isVisible()) { continue; } Rectangle splitChildBounds = splitChild.getBounds(); double splitChildWeight = splitChild.getWeight(); if (!hasMoreVisibleSiblings(splitChild)) { 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(); } } /* 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. */ else { double y = bounds.getY(); double extraHeight = bounds.getHeight() - splitBounds.getHeight(); double availableHeight = extraHeight; while (splitChildren.hasNext()) { Node splitChild = splitChildren.next(); if (!splitChild.isVisible()) { continue; } 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.isVisible()) { return; } 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 = null; int divSize = getDividerSize(); boolean initSplit = false; /* 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(); if (!splitChild.isVisible()) { if (splitChildren.hasNext()) { splitChildren.next(); } continue; } Divider dividerChild = splitChildren.hasNext() ? (Divider) splitChildren.next() : null; double childWidth = 0.0; if (getFloatingDividers()) { childWidth = preferredNodeSize(splitChild).getWidth(); } else { if (dividerChild != null && dividerChild.isVisible()) { double cw = dividerChild.getBounds().getX() - x; if (cw > 0.0) { childWidth = cw; } else { childWidth = preferredNodeSize(splitChild).getWidth(); initSplit = true; } } else { childWidth = split.getBounds().getMaxX() - x; } } childBounds = boundsWithXandWidth(bounds, x, childWidth); layout1(splitChild, childBounds); if ((initSplit || getFloatingDividers()) && dividerChild != null && dividerChild.isVisible()) { double dividerX = childBounds.getMaxX(); Rectangle dividerBounds; dividerBounds = boundsWithXandWidth(bounds, dividerX, divSize); dividerChild.setBounds(dividerBounds); } if (dividerChild != null && dividerChild.isVisible()) { x = dividerChild.getBounds().getMaxX(); } } } /* 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. */ else { double y = bounds.getY(); while (splitChildren.hasNext()) { Node splitChild = splitChildren.next(); if (!splitChild.isVisible()) { if (splitChildren.hasNext()) { splitChildren.next(); } continue; } Divider dividerChild = splitChildren.hasNext() ? (Divider) splitChildren.next() : null; double childHeight = 0.0; if (getFloatingDividers()) { childHeight = preferredNodeSize(splitChild).getHeight(); } else { if (dividerChild != null && dividerChild.isVisible()) { double cy = dividerChild.getBounds().getY() - y; if (cy > 0.0) { childHeight = cy; } else { childHeight = preferredNodeSize(splitChild).getHeight(); initSplit = true; } } else { childHeight = split.getBounds().getMaxY() - y; } } childBounds = boundsWithYandHeight(bounds, y, childHeight); layout1(splitChild, childBounds); if ((initSplit || getFloatingDividers()) && dividerChild != null && dividerChild.isVisible()) { double dividerY = childBounds.getMaxY(); Rectangle dividerBounds = boundsWithYandHeight(bounds, dividerY, divSize); dividerChild.setBounds(dividerBounds); } if (dividerChild != null && dividerChild.isVisible()) { 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); } } /** * Get the layout mode * * @return current layout mode */ public int getLayoutMode() { return layoutMode; } /** * Set the layout mode. By default this layout uses the preferred and minimum sizes of the child components. To ignore the minimum size * set the layout mode to MultiSplitLayout.LAYOUT_NO_MIN_SIZE. * * @param layoutMode * the layout mode * <ul> * <li>DEFAULT_LAYOUT - use the preferred and minimum sizes when sizing the children</li> * <li>LAYOUT_NO_MIN_SIZE - ignore the minimum size when sizing the children</li> </li> */ public void setLayoutMode(int layoutMode) { this.layoutMode = layoutMode; } /** * Get the minimum node size * * @return the minimum size */ public int getUserMinSize() { return userMinSize; } /** * Set the user defined minimum size support in the USER_MIN_SIZE_LAYOUT layout mode. * * @param minSize * the new minimum size */ public void setUserMinSize(int minSize) { userMinSize = minSize; } /** * Get the layoutByWeight falg. If the flag is true the layout initializes itself using the model weights * * @return the layoutByWeight */ public boolean getLayoutByWeight() { return layoutByWeight; } /** * Sset the layoutByWeight falg. If the flag is true the layout initializes itself using the model weights * * @param state * the new layoutByWeight to set */ public void setLayoutByWeight(boolean state) { layoutByWeight = state; } /** * The specified Node is either the wrong type or was configured incorrectly. */ public static class InvalidLayoutException extends RuntimeException { private final Node node; public InvalidLayoutException(String msg, Node node) { super(msg); this.node = node; } /** * @return the invalid Node. */ public Node getNode() { return node; } } private void throwInvalidLayout(String msg, Node node) { throw new InvalidLayoutException(msg, node); } /*public void normalizeSplitWeights() { if (getModel() instanceof Split) { normalizeSplitWeights((Split) getModel()); } }*/ private void normalizeSplitWeights(Split split) { float totalWeight = 0; for (Node n : split.getChildren()) { if (n.isVisible() && !(n instanceof Divider)) { totalWeight += n.getWeight(); } } if (totalWeight > 1) { for (Node n : split.getChildren()) { if (n.isVisible() && !(n instanceof Divider)) { n.setWeight(n.getWeight() / totalWeight); } if (n instanceof Split) { normalizeSplitWeights((Split) n); } } } } private 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.isVisible()) { if (splitChildren.hasNext()) { splitChildren.next(); } continue; } if (splitChild instanceof Divider) { continue; // 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) { normalizeSplitWeights(split); // 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) { if (layoutByWeight && floatingDividers) { doLayoutByWeight(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 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.isVisible()) { continue; } 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 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 List<Divider> dividersThatOverlap(Node root, Rectangle r) { if (nodeOverlapsRectangle(root, r) && root instanceof Split) { List<Divider> dividers = new ArrayList<Divider>(); 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) { if (r == null) { throw new IllegalArgumentException("null Rectangle"); } return dividersThatOverlap(getModel(), r); } /** * Base class for the nodes that model a MultiSplitLayout. */ public static abstract class Node implements Serializable, XMLSerializable { private Split parent = null; private Rectangle bounds = new Rectangle(); private double weight = 0.0; private boolean isVisible = true; public void setVisible(boolean b) { isVisible = b; } /** * Determines whether this node should be visible when its parent is visible. Nodes are initially visible * * @return <code>true</code> if the node is visible, <code>false</code> otherwise */ public boolean isVisible() { return isVisible; } /** * Returns the Split parent of this Node, or null. * * @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. * * @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) { if (bounds == null) { throw new IllegalArgumentException("null 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 * @see #getWeight * @see MultiSplitLayout#layoutContainer * @throws IllegalArgumentException * if weight is not between 0.0 and 1.0 */ 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 p = getParent(); if (p == null) { return null; } List<Node> siblings = p.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); } private Node visibleSiblingAtOffset(int direction, boolean includeDivider) { Split p = getParent(); if (p == null) { return null; } List<Node> siblings = p.getChildren(); int index = siblings.indexOf(this); if (direction > 0) { index++; for (; index < siblings.size(); index++) { Node s = siblings.get(index); if (s.isVisible()) { if (includeDivider || !(s instanceof Divider)) { return s; } } } } else { index--; for (; index >= 0; index--) { Node s = siblings.get(index); if (s.isVisible()) { if (includeDivider || !(s instanceof Divider)) { return s; } } } } return null; } public Node nextVisibleSibling(boolean includeDivider) { return visibleSiblingAtOffset(+1, includeDivider); } public Node previousVisibleSibling(boolean includeDivider) { return visibleSiblingAtOffset(-1, includeDivider); } public abstract String getName(); public void setName(String name) { } } public static class RowSplit extends Split { public RowSplit() { } public RowSplit(Node... children) { setChildren(children); } /** * 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 */ @Override public final boolean isRowLayout() { return true; } } public static class ColSplit extends Split { public ColSplit() { } public ColSplit(Node... children) { setChildren(children); } /** * 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 */ @Override public final boolean isRowLayout() { return false; } } /** * 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; private String name; public Split(Node... children) { setChildren(children); } /** * Default constructor to support xml (de)serialization and other bean spec dependent ops. Resulting instance of Split is invalid * until setChildren() is called. */ public Split() { } /** * Determines whether this node should be visible when its parent is visible. Nodes are initially visible * * @return <code>true</code> if the node is visible, <code>false</code> otherwise */ @Override public boolean isVisible() { for (Node child : children) { if (child.isVisible() && !(child instanceof Divider)) { return true; } } return false; } /** * 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<Node>(children); } /** * Remove a node from the layout. Any sibling dividers will also be removed * * @param n * the node to be removed */ public void remove(Node n) { if (n.nextSibling() instanceof Divider) { children.remove(n.nextSibling()); } else if (n.previousSibling() instanceof Divider) { children.remove(n.previousSibling()); } children.remove(n); } /** * Replace one node with another. This method is used when a child is removed from a split and the split is no longer required, in * which case the remaining node in the child split can replace the split in the parent node * * @param target * the node being replaced * @param replacement * the replacement node */ public void replace(Node target, Node replacement) { int idx = children.indexOf(target); children.remove(target); children.add(idx, replacement); replacement.setParent(this); target.setParent(this); } /** * Change a node to being hidden. Any associated divider nodes are also hidden * * @param target * the node to hide */ public void hide(Node target) { Node next = target.nextSibling(); if (next instanceof Divider) { next.setVisible(false); } else { Node prev = target.previousSibling(); if (prev instanceof Divider) { prev.setVisible(false); } } target.setVisible(false); } /** * Check the dividers to ensure that redundant dividers are hidden and do not interfere in the layout, for example when all the * children of a split are hidden (the split is then invisible), so two dividers may otherwise appear next to one another. * * @param split * the split to check */ public void checkDividers(Split split) { ListIterator<Node> splitChildren = split.getChildren().listIterator(); while (splitChildren.hasNext()) { Node splitChild = splitChildren.next(); if (!splitChild.isVisible()) { continue; } else if (splitChildren.hasNext()) { Node dividerChild = splitChildren.next(); if (dividerChild instanceof Divider) { if (splitChildren.hasNext()) { Node rightChild = splitChildren.next(); while (!rightChild.isVisible()) { rightChild = rightChild.nextSibling(); if (rightChild == null) { // No visible right sibling found, so hide the divider dividerChild.setVisible(false); break; } } // A visible child is found but it's a divider and therefore // we have to visible and adjacent dividers - so we hide one if (rightChild != null && rightChild instanceof Divider) { dividerChild.setVisible(false); } } } else if (splitChild instanceof Divider && dividerChild instanceof Divider) { splitChild.setVisible(false); } } } } /** * Restore any of the hidden dividers that are required to separate visible nodes * * @param split * the node to check */ public void restoreDividers(Split split) { boolean nextDividerVisible = false; ListIterator<Node> splitChildren = split.getChildren().listIterator(); while (splitChildren.hasNext()) { Node splitChild = splitChildren.next(); if (splitChild instanceof Divider) { Node prev = splitChild.previousSibling(); if (prev.isVisible()) { Node next = splitChild.nextSibling(); while (next != null) { if (next.isVisible()) { splitChild.setVisible(true); break; } next = next.nextSibling(); } } } } if (split.getParent() != null) { restoreDividers(split.getParent()); } } /** * 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 * @see #getChildren * @throws IllegalArgumentException * if children is null */ 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<Node>(children); for (Node child : this.children) { child.setParent(this); } } public void addToChildren(Node child) { children.add(child); child.setParent(this); } public void removeFromChildren(Node child) { child.setParent(null); children.remove(child); } /** * Convenience method for setting the children 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 array. * * @param children * array of children * @see #getChildren * @throws IllegalArgumentException * if children is null */ public void setChildren(Node... children) { setChildren(children == null ? null : Arrays.asList(children)); } /** * 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> kids = getChildren(); Node weightedChild = null; for (Node child : kids) { if (!child.isVisible()) { continue; } if (child.getWeight() > 0.0) { weightedChild = child; } } return weightedChild; } /** * Return the Leaf's name. * * @return the value of the name property. * @see #setName */ @Override public String getName() { if (StringUtils.isEmpty(name)) { return getClass().getSimpleName(); } 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 */ @Override public void setName(String name) { if (name == null) { throw new IllegalArgumentException("name is null"); } this.name = name; } @Override public String toString() { int nChildren = getChildren().size(); StringBuffer sb = new StringBuffer("MultiSplitLayout.Split"); sb.append(" \""); sb.append(getName()); sb.append("\""); sb.append(isRowLayout() ? " ROW [" : " COLUMN ["); sb.append(nChildren + (nChildren == 1 ? " child" : " children")); sb.append("] "); sb.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() { } /** * 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) { if (name == null) { throw new IllegalArgumentException("name is null"); } this.name = name; } /** * Return the Leaf's name. * * @return the value of the name property. * @see #setName */ @Override 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 */ @Override public void setName(String name) { if (name == null) { throw new IllegalArgumentException("name is null"); } this.name = name; } @Override public String toString() { StringBuffer sb = new StringBuffer("MultiSplitLayout.Leaf"); sb.append(" \""); sb.append(getName()); sb.append("\""); sb.append(" weight="); sb.append(getWeight()); sb.append(" "); sb.append(getBounds()); return sb.toString(); } } /** * Models a single vertical/horiztonal divider. */ public static class Divider extends Node { @Override public String getName() { return "Divider"; } /** * 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() : false; } /** * Dividers can't have a weight, they don't grow or shrink. * * @throws UnsupportedOperationException */ @Override public void setWeight(double weight) { throw new UnsupportedOperationException(); } @Override public String toString() { return "MultiSplitLayout.Divider " + getBounds().toString(); } } private static void throwParseException(StreamTokenizer st, String msg) throws Exception { throw new Exception("MultiSplitLayout.parseModel Error: " + msg); } private static void parseAttribute(String name, StreamTokenizer st, Node node) throws Exception { if (st.nextToken() != '=') { throwParseException(st, "expected '=' after " + name); } if (name.equalsIgnoreCase("WEIGHT")) { if (st.nextToken() == StreamTokenizer.TT_NUMBER) { node.setWeight(st.nval); } else { throwParseException(st, "invalid weight"); } } else if (name.equalsIgnoreCase("NAME")) { if (st.nextToken() == StreamTokenizer.TT_WORD) { if (node instanceof Leaf) { ((Leaf) node).setName(st.sval); } else if (node instanceof Split) { ((Split) node).setName(st.sval); } else { throwParseException(st, "can't specify name for " + node); } } else { throwParseException(st, "invalid name"); } } else { throwParseException(st, "unrecognized attribute \"" + name + "\""); } } private static void addSplitChild(Split parent, Node child) { List<Node> children = new ArrayList<Node>(parent.getChildren()); if (children.size() == 0) { children.add(child); } else { children.add(new Divider()); children.add(child); } parent.setChildren(children); } private static void parseLeaf(StreamTokenizer st, Split parent) throws Exception { Leaf leaf = new Leaf(); int token; while ((token = st.nextToken()) != StreamTokenizer.TT_EOF) { if (token == ')') { break; } if (token == StreamTokenizer.TT_WORD) { parseAttribute(st.sval, st, leaf); } else { throwParseException(st, "Bad Leaf: " + leaf); } } addSplitChild(parent, leaf); } private static void parseSplit(StreamTokenizer st, Split parent) throws Exception { int token; while ((token = st.nextToken()) != StreamTokenizer.TT_EOF) { if (token == ')') { break; } else if (token == StreamTokenizer.TT_WORD) { if (st.sval.equalsIgnoreCase("WEIGHT")) { parseAttribute(st.sval, st, parent); } else if (st.sval.equalsIgnoreCase("NAME")) { parseAttribute(st.sval, st, parent); } else { addSplitChild(parent, new Leaf(st.sval)); } } else if (token == '(') { if ((token = st.nextToken()) != StreamTokenizer.TT_WORD) { throwParseException(st, "invalid node type"); } String nodeType = st.sval.toUpperCase(); if (nodeType.equals("LEAF")) { parseLeaf(st, parent); } else if (nodeType.equals("ROW") || nodeType.equals("COLUMN")) { Split split = new Split(); split.setRowLayout(nodeType.equals("ROW")); addSplitChild(parent, split); parseSplit(st, split); } else { throwParseException(st, "unrecognized node type '" + nodeType + "'"); } } } } private static Node parseModel(Reader r) { StreamTokenizer st = new StreamTokenizer(r); try { Split root = new Split(); parseSplit(st, root); return root.getChildren().get(0); } catch (Exception e) { System.err.println(e); } finally { try { r.close(); } catch (IOException ignore) { } } return null; } /** * A convenience method that converts a string to a MultiSplitLayout model (a tree of Nodes) using a a simple syntax. Nodes are * represented by parenthetical expressions whose first token is one of ROW/COLUMN/LEAF. ROW and COLUMN specify horizontal and vertical * Split nodes respectively, LEAF specifies a Leaf node. A Leaf's name and weight can be specified with attributes, * name=<i>myLeafName</i> weight=<i>myLeafWeight</i>. Similarly, a Split's weight can be specified with weight=<i>mySplitWeight</i>. * * <p> * For example, the following expression generates a horizontal Split node with three children: the Leafs named left and right, and a * Divider in between: * * <pre> * (ROW (LEAF name=left) (LEAF name=right weight=1.0)) * </pre> * * <p> * Dividers should not be included in the string, they're added automatcially as needed. Because Leaf nodes often only need to specify a * name, one can specify a Leaf by just providing the name. The previous example can be written like this: * * <pre> * (ROW left (LEAF name=right weight=1.0)) * </pre> * * <p> * Here's a more complex example. One row with three elements, the first and last of which are columns with two leaves each: * * <pre> * (ROW (COLUMN weight=0.5 left.top left.bottom) * (LEAF name=middle) * (COLUMN weight=0.5 right.top right.bottom)) * </pre> * * * <p> * This syntax is not intended for archiving or configuration files . It's just a convenience for examples and tests. * * @return the Node root of a tree based on s. */ public static Node parseModel(String s) { return parseModel(new StringReader(s)); } private static void printModel(String indent, Node root) { if (root instanceof Split) { Split split = (Split) root; System.out.println(indent + split); for (Node child : split.getChildren()) { printModel(indent + " ", child); } } else { System.out.println(indent + root); } } /** * Print the tree with enough detail for simple debugging. */ public static void printModel(Node root) { printModel("", root); } }