All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jdesktop.swingx.MultiSplitLayout Maven / Gradle / Ivy

/*
 * $Id$
 *
 * 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.jdesktop.swingx;

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;


/**
 * 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.
 *
 * 

* 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. * *

* 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 childMap = new HashMap(); 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 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 if (model instanceof Leaf) { if (((Leaf) model).getName().equals(name)) { return model; } else { return null; } } 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 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 * name should match the name property of the Leaf * node that represents the bounds of child. 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 name. 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 ); 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 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 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 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 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 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 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 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 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 *

    *
  • DEFAULT_LAYOUT - use the preferred and minimum sizes when sizing the children
  • *
  • LAYOUT_NO_MIN_SIZE - ignore the minimum size when sizing the children
  • * */ 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); } 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 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) { 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 dividersThatOverlap(Node root, Rectangle r) { if (nodeOverlapsRectangle(root, r) && (root instanceof Split)) { List 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 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 { 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 true if the node is visible, * false 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 new Rectangle(0,0,0,0). * * @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 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); } } 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 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 true if the node is visible, * false 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 getChildren() { return new ArrayList(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 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 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 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 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 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 */ 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) { 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 */ 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) { 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 { /** * 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 children = new ArrayList(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=myLeafName weight=myLeafWeight. * Similarly, a Split's weight can be specified with * weight=mySplitWeight. * *

    For example, the following expression generates * a horizontal Split node with three children: * the Leafs named left and right, and a Divider in * between: *

       * (ROW (LEAF name=left) (LEAF name=right weight=1.0))
       * 
    * *

    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: *

       * (ROW left (LEAF name=right weight=1.0))
       * 
    * *

    Here's a more complex example. One row with * three elements, the first and last of which are columns * with two leaves each: *

       * (ROW (COLUMN weight=0.5 left.top left.bottom)
       *      (LEAF name=middle)
       *      (COLUMN weight=0.5 right.top right.bottom))
       * 
    * * *

    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); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy