001    /*
002     * $Id: MultiSplitLayout.java,v 1.1 2006/03/23 21:52:43 hansmuller Exp $
003     *
004     * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005     * Santa Clara, California 95054, U.S.A. All rights reserved.
006     *
007     * This library is free software; you can redistribute it and/or
008     * modify it under the terms of the GNU Lesser General Public
009     * License as published by the Free Software Foundation; either
010     * version 2.1 of the License, or (at your option) any later version.
011     * 
012     * This library is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015     * Lesser General Public License for more details.
016     * 
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this library; if not, write to the Free Software
019     * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020     */
021    
022    package org.jdesktop.swingx;
023    
024    import java.awt.Component;
025    import java.awt.Container;
026    import java.awt.Dimension;
027    import java.awt.Insets;
028    import java.awt.LayoutManager;
029    import java.awt.Rectangle;
030    import java.beans.PropertyChangeListener;
031    import java.beans.PropertyChangeSupport;
032    import java.io.IOException;
033    import java.io.Reader;
034    import java.io.StreamTokenizer;
035    import java.io.StringReader;
036    import java.util.ArrayList;
037    import java.util.Collections;
038    import java.util.HashMap;
039    import java.util.Iterator;
040    import java.util.List;
041    import java.util.ListIterator;
042    import java.util.Map;
043    import javax.swing.UIManager;
044    
045    
046    /**
047     * The MultiSplitLayout layout manager recursively arranges its
048     * components in row and column groups called "Splits".  Elements of
049     * the layout are separated by gaps called "Dividers".  The overall
050     * layout is defined with a simple tree model whose nodes are 
051     * instances of MultiSplitLayout.Split, MultiSplitLayout.Divider, 
052     * and MultiSplitLayout.Leaf. Named Leaf nodes represent the space 
053     * allocated to a component that was added with a constraint that
054     * matches the Leaf's name.  Extra space is distributed
055     * among row/column siblings according to their 0.0 to 1.0 weight.
056     * If no weights are specified then the last sibling always gets
057     * all of the extra space, or space reduction.
058     * 
059     * <p>
060     * Although MultiSplitLayout can be used with any Container, it's
061     * the default layout manager for JXMultiSplitPane.  JXMultiSplitPane
062     * supports interactively dragging the Dividers, accessibility, 
063     * and other features associated with split panes.
064     * 
065     * <p>
066     * All properties in this class are bound: when a properties value
067     * is changed, all PropertyChangeListeners are fired.
068     * 
069     * @author Hans Muller
070     * @see JXMultiSplitPane
071     */
072    
073    public class MultiSplitLayout implements LayoutManager {
074        private final Map<String, Component> childMap = new HashMap<String, Component>();
075        private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
076        private Node model;
077        private int dividerSize;
078        private boolean floatingDividers = true;
079    
080        /**
081         * Create a MultiSplitLayout with a default model with a single
082         * Leaf node named "default".  
083         * 
084         * #see setModel
085         */
086        public MultiSplitLayout() { 
087        this(new Leaf("default"));
088        }
089        
090        /**
091         * Create a MultiSplitLayout with the specified model.
092         * 
093         * #see setModel
094         */
095        public MultiSplitLayout(Node model) {
096        this.model = model;
097            this.dividerSize = UIManager.getInt("SplitPane.dividerSize"); 
098        if (this.dividerSize == 0) {
099                this.dividerSize = 7;
100            }
101        }
102    
103        public void addPropertyChangeListener(PropertyChangeListener listener) {
104            if (listener != null) {
105            pcs.addPropertyChangeListener(listener);
106            }
107        }
108        public void removePropertyChangeListener(PropertyChangeListener listener) {
109            if (listener != null) {
110            pcs.removePropertyChangeListener(listener);
111            }
112        }
113        public PropertyChangeListener[] getPropertyChangeListeners() {
114            return pcs.getPropertyChangeListeners();
115        }
116    
117        private void firePCS(String propertyName, Object oldValue, Object newValue) {
118        if (!(oldValue != null && newValue != null && oldValue.equals(newValue))) {
119            pcs.firePropertyChange(propertyName, oldValue, newValue);
120            }
121        }
122    
123        /**
124         * Return the root of the tree of Split, Leaf, and Divider nodes
125         * that define this layout.  
126         * 
127         * @return the value of the model property
128         * @see #setModel
129         */
130        public Node getModel() { return model; }
131    
132        /**
133         * Set the root of the tree of Split, Leaf, and Divider nodes
134         * that define this layout.  The model can be a Split node
135         * (the typical case) or a Leaf.  The default value of this
136         * property is a Leaf named "default".
137         * 
138         * @param model the root of the tree of Split, Leaf, and Divider node
139         * @throws IllegalArgumentException if model is a Divider or null
140         * @see #getModel
141         */
142        public void setModel(Node model) {
143        if ((model == null) || (model instanceof Divider)) {
144            throw new IllegalArgumentException("invalid model");
145        }
146        Node oldModel = model;
147        this.model = model;
148        firePCS("model", oldModel, model);
149        }
150    
151        /**
152         * Returns the width of Dividers in Split rows, and the height of 
153         * Dividers in Split columns.
154         *
155         * @return the value of the dividerSize property
156         * @see #setDividerSize
157         */
158        public int getDividerSize() { return dividerSize; }
159    
160        /**
161         * Sets the width of Dividers in Split rows, and the height of 
162         * Dividers in Split columns.  The default value of this property
163         * is the same as for JSplitPane Dividers.
164         *
165         * @param dividerSize the size of dividers (pixels)
166         * @throws IllegalArgumentException if dividerSize < 0
167         * @see #getDividerSize
168         */
169        public void setDividerSize(int dividerSize) {
170        if (dividerSize < 0) {
171            throw new IllegalArgumentException("invalid dividerSize");
172        }
173        int oldDividerSize = this.dividerSize;
174        this.dividerSize = dividerSize;
175        firePCS("dividerSize", oldDividerSize, dividerSize);
176        }
177    
178        /**
179         * @return the value of the floatingDividers property
180         * @see #setFloatingDividers
181         */
182        public boolean getFloatingDividers() { return floatingDividers; }
183    
184    
185        /**
186         * If true, Leaf node bounds match the corresponding component's 
187         * preferred size and Splits/Dividers are resized accordingly.  
188         * If false then the Dividers define the bounds of the adjacent
189         * Split and Leaf nodes.  Typically this property is set to false
190         * after the (MultiSplitPane) user has dragged a Divider.
191         * 
192         * @see #getFloatingDividers
193         */
194        public void setFloatingDividers(boolean floatingDividers) {
195        boolean oldFloatingDividers = this.floatingDividers;
196        this.floatingDividers = floatingDividers;
197        firePCS("floatingDividers", oldFloatingDividers, floatingDividers);
198        }
199    
200    
201        /** 
202         * Add a component to this MultiSplitLayout.  The
203         * <code>name</code> should match the name property of the Leaf
204         * node that represents the bounds of <code>child</code>.  After
205         * layoutContainer() recomputes the bounds of all of the nodes in
206         * the model, it will set this child's bounds to the bounds of the
207         * Leaf node with <code>name</code>.  Note: if a component was already
208         * added with the same name, this method does not remove it from 
209         * its parent.  
210         * 
211         * @param name identifies the Leaf node that defines the child's bounds
212         * @param child the component to be added
213         * @see #removeLayoutComponent
214         */
215        public void addLayoutComponent(String name, Component child) {
216        if (name == null) {
217            throw new IllegalArgumentException("name not specified");
218        }
219        childMap.put(name, child);
220        }
221    
222        /**
223         * Removes the specified component from the layout.
224         * 
225         * @param child the component to be removed
226         * @see #addLayoutComponent
227         */
228        public void removeLayoutComponent(Component child) {
229        String name = child.getName();
230        if (name != null) {
231            childMap.remove(name);
232        }
233        }
234    
235        private Component childForNode(Node node) {
236        if (node instanceof Leaf) {
237            Leaf leaf = (Leaf)node;
238            String name = leaf.getName();
239            return (name != null) ? childMap.get(name) : null;
240        }
241        return null;
242        }
243    
244    
245        private Dimension preferredComponentSize(Node node) {
246        Component child = childForNode(node);
247        return (child != null) ? child.getPreferredSize() : new Dimension(0, 0);
248        
249        }
250    
251        private Dimension minimumComponentSize(Node node) {
252        Component child = childForNode(node);
253        return (child != null) ? child.getMinimumSize() : new Dimension(0, 0);
254        
255        }
256    
257        private Dimension preferredNodeSize(Node root) {
258        if (root instanceof Leaf) {
259            return preferredComponentSize(root);
260        }
261        else if (root instanceof Divider) {
262            int dividerSize = getDividerSize();
263            return new Dimension(dividerSize, dividerSize);
264        }
265        else {
266            Split split = (Split)root;
267            List<Node> splitChildren = split.getChildren();
268            int width = 0;
269            int height = 0;
270            if (split.isRowLayout()) { 
271            for(Node splitChild : splitChildren) {
272                Dimension size = preferredNodeSize(splitChild);
273                width += size.width;
274                height = Math.max(height, size.height);
275            }
276            }
277            else {
278            for(Node splitChild : splitChildren) {
279                Dimension size = preferredNodeSize(splitChild);
280                width = Math.max(width, size.width);
281                height += size.height;
282            }
283            }
284            return new Dimension(width, height);
285        }
286        }
287    
288        private Dimension minimumNodeSize(Node root) {
289        if (root instanceof Leaf) {
290            Component child = childForNode(root);
291            return (child != null) ? child.getMinimumSize() : new Dimension(0, 0);
292        }
293        else if (root instanceof Divider) {
294            int dividerSize = getDividerSize();
295            return new Dimension(dividerSize, dividerSize);
296        }
297        else {
298            Split split = (Split)root;
299            List<Node> splitChildren = split.getChildren();
300            int width = 0;
301            int height = 0;
302            if (split.isRowLayout()) { 
303            for(Node splitChild : splitChildren) {
304                Dimension size = minimumNodeSize(splitChild);
305                width += size.width;
306                height = Math.max(height, size.height);
307            }
308            }
309            else {
310            for(Node splitChild : splitChildren) {
311                Dimension size = minimumNodeSize(splitChild);
312                width = Math.max(width, size.width);
313                height += size.height;
314            }
315            }
316            return new Dimension(width, height);
317        }
318        }
319    
320        private Dimension sizeWithInsets(Container parent, Dimension size) {
321        Insets insets = parent.getInsets();
322        int width = size.width + insets.left + insets.right;
323        int height = size.height + insets.top + insets.bottom;
324        return new Dimension(width, height);
325        }
326    
327        public Dimension preferredLayoutSize(Container parent) {
328        Dimension size = preferredNodeSize(getModel());
329        return sizeWithInsets(parent, size);
330        }
331    
332        public Dimension minimumLayoutSize(Container parent) {
333        Dimension size = minimumNodeSize(getModel());
334        return sizeWithInsets(parent, size);
335        }
336    
337    
338        private Rectangle boundsWithYandHeight(Rectangle bounds, double y, double height) {
339        Rectangle r = new Rectangle();
340        r.setBounds((int)(bounds.getX()), (int)y, (int)(bounds.getWidth()), (int)height);
341        return r;
342        }
343    
344        private Rectangle boundsWithXandWidth(Rectangle bounds, double x, double width) {
345        Rectangle r = new Rectangle();
346        r.setBounds((int)x, (int)(bounds.getY()), (int)width, (int)(bounds.getHeight()));
347        return r;
348        }
349    
350    
351        private void minimizeSplitBounds(Split split, Rectangle bounds) {
352        Rectangle splitBounds = new Rectangle(bounds.x, bounds.y, 0, 0);
353        List<Node> splitChildren = split.getChildren();
354        Node lastChild = splitChildren.get(splitChildren.size() - 1);
355        Rectangle lastChildBounds = lastChild.getBounds();
356        if (split.isRowLayout()) {
357            int lastChildMaxX = lastChildBounds.x + lastChildBounds.width;
358            splitBounds.add(lastChildMaxX, bounds.y + bounds.height);
359        }
360        else {
361            int lastChildMaxY = lastChildBounds.y + lastChildBounds.height;
362            splitBounds.add(bounds.x + bounds.width, lastChildMaxY);
363        }
364        split.setBounds(splitBounds);
365        }
366    
367    
368        private void layoutShrink(Split split, Rectangle bounds) {
369        Rectangle splitBounds = split.getBounds();
370        ListIterator<Node> splitChildren = split.getChildren().listIterator();
371        Node lastWeightedChild = split.lastWeightedChild();
372    
373        if (split.isRowLayout()) {
374            int totalWidth = 0;          // sum of the children's widths
375            int minWeightedWidth = 0;    // sum of the weighted childrens' min widths
376            int totalWeightedWidth = 0;  // sum of the weighted childrens' widths
377            for(Node splitChild : split.getChildren()) {
378            int nodeWidth = splitChild.getBounds().width;
379            int nodeMinWidth = Math.min(nodeWidth, minimumNodeSize(splitChild).width);
380            totalWidth += nodeWidth;
381            if (splitChild.getWeight() > 0.0) {
382                minWeightedWidth += nodeMinWidth;
383                totalWeightedWidth += nodeWidth;
384            }
385            }
386    
387            double x = bounds.getX();
388            double extraWidth = splitBounds.getWidth() - bounds.getWidth();
389            double availableWidth = extraWidth;
390            boolean onlyShrinkWeightedComponents = 
391            (totalWeightedWidth - minWeightedWidth) > extraWidth;
392    
393            while(splitChildren.hasNext()) {
394            Node splitChild = splitChildren.next();
395            Rectangle splitChildBounds = splitChild.getBounds();
396            double minSplitChildWidth = minimumNodeSize(splitChild).getWidth();
397            double splitChildWeight = (onlyShrinkWeightedComponents)
398                ? splitChild.getWeight()
399                : (splitChildBounds.getWidth() / (double)totalWidth);
400    
401            if (!splitChildren.hasNext()) {
402                double newWidth =  Math.max(minSplitChildWidth, bounds.getMaxX() - x); 
403                Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
404                layout2(splitChild, newSplitChildBounds);
405            }
406            else if ((availableWidth > 0.0) && (splitChildWeight > 0.0)) {
407                double allocatedWidth = Math.rint(splitChildWeight * extraWidth);
408                double oldWidth = splitChildBounds.getWidth();
409                double newWidth = Math.max(minSplitChildWidth, oldWidth - allocatedWidth);
410                Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
411                layout2(splitChild, newSplitChildBounds);
412                availableWidth -= (oldWidth - splitChild.getBounds().getWidth());
413            }
414            else {
415                double existingWidth = splitChildBounds.getWidth();
416                Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, existingWidth);
417                layout2(splitChild, newSplitChildBounds);
418            }
419            x = splitChild.getBounds().getMaxX();
420            }
421        }
422    
423        else {
424            int totalHeight = 0;          // sum of the children's heights
425            int minWeightedHeight = 0;    // sum of the weighted childrens' min heights
426            int totalWeightedHeight = 0;  // sum of the weighted childrens' heights
427            for(Node splitChild : split.getChildren()) {
428            int nodeHeight = splitChild.getBounds().height;
429            int nodeMinHeight = Math.min(nodeHeight, minimumNodeSize(splitChild).height);
430            totalHeight += nodeHeight;
431            if (splitChild.getWeight() > 0.0) {
432                minWeightedHeight += nodeMinHeight;
433                totalWeightedHeight += nodeHeight;
434            }
435            }
436    
437            double y = bounds.getY();
438            double extraHeight = splitBounds.getHeight() - bounds.getHeight();
439            double availableHeight = extraHeight;
440            boolean onlyShrinkWeightedComponents = 
441            (totalWeightedHeight - minWeightedHeight) > extraHeight;
442    
443            while(splitChildren.hasNext()) {
444            Node splitChild = splitChildren.next();
445            Rectangle splitChildBounds = splitChild.getBounds();
446            double minSplitChildHeight = minimumNodeSize(splitChild).getHeight();
447            double splitChildWeight = (onlyShrinkWeightedComponents)
448                ? splitChild.getWeight()
449                : (splitChildBounds.getHeight() / (double)totalHeight);
450    
451            if (!splitChildren.hasNext()) {
452                double oldHeight = splitChildBounds.getHeight();
453                double newHeight =  Math.max(minSplitChildHeight, bounds.getMaxY() - y); 
454                Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
455                layout2(splitChild, newSplitChildBounds);
456                availableHeight -= (oldHeight - splitChild.getBounds().getHeight());
457            }
458            else if ((availableHeight > 0.0) && (splitChildWeight > 0.0)) {
459                double allocatedHeight = Math.rint(splitChildWeight * extraHeight);
460                double oldHeight = splitChildBounds.getHeight();
461                double newHeight = Math.max(minSplitChildHeight, oldHeight - allocatedHeight);
462                Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
463                layout2(splitChild, newSplitChildBounds);
464                availableHeight -= (oldHeight - splitChild.getBounds().getHeight());
465            }
466            else {
467                double existingHeight = splitChildBounds.getHeight();
468                Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, existingHeight);
469                layout2(splitChild, newSplitChildBounds);
470            }
471            y = splitChild.getBounds().getMaxY();
472            }
473        }
474    
475        /* The bounds of the Split node root are set to be 
476         * big enough to contain all of its children. Since 
477         * Leaf children can't be reduced below their 
478         * (corresponding java.awt.Component) minimum sizes, 
479         * the size of the Split's bounds maybe be larger than
480         * the bounds we were asked to fit within.
481         */
482        minimizeSplitBounds(split, bounds);
483        }
484    
485    
486        private void layoutGrow(Split split, Rectangle bounds) {
487        Rectangle splitBounds = split.getBounds();
488        ListIterator<Node> splitChildren = split.getChildren().listIterator();
489        Node lastWeightedChild = split.lastWeightedChild();
490    
491        /* Layout the Split's child Nodes' along the X axis.  The bounds 
492         * of each child will have the same y coordinate and height as the 
493         * layoutGrow() bounds argument.  Extra width is allocated to the 
494         * to each child with a non-zero weight:
495         *     newWidth = currentWidth + (extraWidth * splitChild.getWeight())
496         * Any extraWidth "left over" (that's availableWidth in the loop
497         * below) is given to the last child.  Note that Dividers always
498         * have a weight of zero, and they're never the last child.
499         */
500        if (split.isRowLayout()) {
501            double x = bounds.getX();
502            double extraWidth = bounds.getWidth() - splitBounds.getWidth();
503            double availableWidth = extraWidth;
504    
505            while(splitChildren.hasNext()) {
506            Node splitChild = splitChildren.next();
507            Rectangle splitChildBounds = splitChild.getBounds();
508            double splitChildWeight = splitChild.getWeight();
509    
510            if (!splitChildren.hasNext()) {  
511                double newWidth = bounds.getMaxX() - x; 
512                Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
513                layout2(splitChild, newSplitChildBounds);
514            }
515            else if ((availableWidth > 0.0) && (splitChildWeight > 0.0)) {
516                double allocatedWidth = (splitChild.equals(lastWeightedChild)) 
517                ? availableWidth
518                : Math.rint(splitChildWeight * extraWidth);
519                double newWidth = splitChildBounds.getWidth() + allocatedWidth;
520                Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
521                layout2(splitChild, newSplitChildBounds);
522                availableWidth -= allocatedWidth;
523            }
524            else {
525                double existingWidth = splitChildBounds.getWidth();
526                Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, existingWidth);
527                layout2(splitChild, newSplitChildBounds);
528            }
529            x = splitChild.getBounds().getMaxX();
530            }
531        }
532    
533        /* Layout the Split's child Nodes' along the Y axis.  The bounds 
534         * of each child will have the same x coordinate and width as the 
535         * layoutGrow() bounds argument.  Extra height is allocated to the 
536         * to each child with a non-zero weight:
537         *     newHeight = currentHeight + (extraHeight * splitChild.getWeight())
538         * Any extraHeight "left over" (that's availableHeight in the loop
539         * below) is given to the last child.  Note that Dividers always
540         * have a weight of zero, and they're never the last child.
541         */
542        else {
543            double y = bounds.getY();
544            double extraHeight = bounds.getMaxY() - splitBounds.getHeight();
545            double availableHeight = extraHeight;
546    
547            while(splitChildren.hasNext()) {
548            Node splitChild = splitChildren.next();
549            Rectangle splitChildBounds = splitChild.getBounds();
550            double splitChildWeight = splitChild.getWeight();
551            
552            if (!splitChildren.hasNext()) {
553                double newHeight = bounds.getMaxY() - y; 
554                Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
555                layout2(splitChild, newSplitChildBounds);
556            }
557            else if ((availableHeight > 0.0) && (splitChildWeight > 0.0)) {
558                double allocatedHeight = (splitChild.equals(lastWeightedChild)) 
559                ? availableHeight
560                : Math.rint(splitChildWeight * extraHeight);
561                double newHeight = splitChildBounds.getHeight() + allocatedHeight;
562                Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
563                layout2(splitChild, newSplitChildBounds);
564                availableHeight -= allocatedHeight;
565            }
566            else {
567                double existingHeight = splitChildBounds.getHeight();
568                Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, existingHeight);
569                layout2(splitChild, newSplitChildBounds);
570            }
571            y = splitChild.getBounds().getMaxY();
572            }
573        }
574        }
575    
576    
577        /* Second pass of the layout algorithm: branch to layoutGrow/Shrink
578         * as needed.
579         */
580       private void layout2(Node root, Rectangle bounds) {
581        if (root instanceof Leaf) {
582            Component child = childForNode(root);
583            if (child != null) {
584            child.setBounds(bounds);
585            }
586            root.setBounds(bounds);
587        }
588        else if (root instanceof Divider) {
589            root.setBounds(bounds);
590        }
591        else if (root instanceof Split) {
592            Split split = (Split)root;
593            boolean grow = split.isRowLayout() 
594            ? (split.getBounds().width <= bounds.width)
595            : (split.getBounds().height <= bounds.height);
596            if (grow) {
597            layoutGrow(split, bounds);
598            root.setBounds(bounds);
599            }
600            else {
601            layoutShrink(split, bounds);
602                    // split.setBounds() called in layoutShrink()
603            }
604        }
605        }
606    
607    
608        /* First pass of the layout algorithm.
609         * 
610         * If the Dividers are "floating" then set the bounds of each
611         * node to accomodate the preferred size of all of the 
612         * Leaf's java.awt.Components.  Otherwise, just set the bounds
613         * of each Leaf/Split node so that it's to the left of (for
614         * Split.isRowLayout() Split children) or directly above
615         * the Divider that follows.
616         * 
617         * This pass sets the bounds of each Node in the layout model.  It
618         * does not resize any of the parent Container's
619         * (java.awt.Component) children.  That's done in the second pass,
620         * see layoutGrow() and layoutShrink().
621         */
622        private void layout1(Node root, Rectangle bounds) {
623        if (root instanceof Leaf) {
624            root.setBounds(bounds);
625        }
626        else if (root instanceof Split) {
627            Split split = (Split)root;
628            Iterator<Node> splitChildren = split.getChildren().iterator();
629            Rectangle childBounds = null;
630            int dividerSize = getDividerSize();
631            
632            /* Layout the Split's child Nodes' along the X axis.  The bounds 
633             * of each child will have the same y coordinate and height as the 
634             * layout1() bounds argument.  
635             * 
636             * Note: the column layout code - that's the "else" clause below
637             * this if, is identical to the X axis (rowLayout) code below.
638             */
639            if (split.isRowLayout()) {
640            double x = bounds.getX();
641            while(splitChildren.hasNext()) {
642                Node splitChild = splitChildren.next();
643                Divider dividerChild = 
644                (splitChildren.hasNext()) ? (Divider)(splitChildren.next()) : null;
645    
646                double childWidth = 0.0;
647                if (getFloatingDividers()) {
648                childWidth = preferredNodeSize(splitChild).getWidth();
649                }
650                else {
651                if (dividerChild != null) {
652                    childWidth = dividerChild.getBounds().getX() - x;
653                }
654                else {
655                    childWidth = split.getBounds().getMaxX() - x;
656                }
657                }
658                childBounds = boundsWithXandWidth(bounds, x, childWidth);
659                layout1(splitChild, childBounds);
660    
661                if (getFloatingDividers() && (dividerChild != null)) {
662                double dividerX = childBounds.getMaxX();
663                Rectangle dividerBounds = boundsWithXandWidth(bounds, dividerX, dividerSize);
664                dividerChild.setBounds(dividerBounds);
665                }
666                if (dividerChild != null) {
667                x = dividerChild.getBounds().getMaxX();
668                }
669            }
670            }
671    
672            /* Layout the Split's child Nodes' along the Y axis.  The bounds 
673             * of each child will have the same x coordinate and width as the 
674             * layout1() bounds argument.  The algorithm is identical to what's
675             * explained above, for the X axis case.
676             */
677            else {
678            double y = bounds.getY();
679            while(splitChildren.hasNext()) {
680                Node splitChild = splitChildren.next();
681                Divider dividerChild = 
682                (splitChildren.hasNext()) ? (Divider)(splitChildren.next()) : null;
683    
684                double childHeight = 0.0;
685                if (getFloatingDividers()) {
686                childHeight = preferredNodeSize(splitChild).getHeight();
687                }
688                else {
689                if (dividerChild != null) {
690                    childHeight = dividerChild.getBounds().getY() - y;
691                }
692                else {
693                    childHeight = split.getBounds().getMaxY() - y;
694                }
695                }
696                childBounds = boundsWithYandHeight(bounds, y, childHeight);
697                layout1(splitChild, childBounds);
698    
699                if (getFloatingDividers() && (dividerChild != null)) {
700                double dividerY = childBounds.getMaxY();
701                Rectangle dividerBounds = boundsWithYandHeight(bounds, dividerY, dividerSize);
702                dividerChild.setBounds(dividerBounds);
703                }
704                if (dividerChild != null) {
705                y = dividerChild.getBounds().getMaxY();
706                }
707            }
708            }
709            /* The bounds of the Split node root are set to be just
710             * big enough to contain all of its children, but only
711             * along the axis it's allocating space on.  That's 
712             * X for rows, Y for columns.  The second pass of the 
713             * layout algorithm - see layoutShrink()/layoutGrow() 
714             * allocates extra space.
715             */
716            minimizeSplitBounds(split, bounds);
717        }
718        }
719    
720        /** 
721         * The specified Node is either the wrong type or was configured
722         * incorrectly.
723         */
724        public static class InvalidLayoutException extends RuntimeException {
725        private final Node node;
726        public InvalidLayoutException (String msg, Node node) {
727            super(msg);
728            this.node = node;
729        }
730        /** 
731         * @return the invalid Node.
732         */
733        public Node getNode() { return node; }
734        }
735    
736        private void throwInvalidLayout(String msg, Node node) {
737        throw new InvalidLayoutException(msg, node);
738        }
739    
740        private void checkLayout(Node root) {
741        if (root instanceof Split) {
742            Split split = (Split)root;
743            if (split.getChildren().size() <= 2) {
744            throwInvalidLayout("Split must have > 2 children", root);
745            }
746            Iterator<Node> splitChildren = split.getChildren().iterator();
747            double weight = 0.0;
748            while(splitChildren.hasNext()) {
749            Node splitChild = splitChildren.next();
750            if (splitChild instanceof Divider) {
751                throwInvalidLayout("expected a Split or Leaf Node", splitChild);
752            }
753            if (splitChildren.hasNext()) {
754                Node dividerChild = splitChildren.next();
755                if (!(dividerChild instanceof Divider)) {
756                throwInvalidLayout("expected a Divider Node", dividerChild);
757                }
758            }
759            weight += splitChild.getWeight();
760            checkLayout(splitChild);
761            }
762            if (weight > 1.0) {
763            throwInvalidLayout("Split children's total weight > 1.0", root);
764            }
765        }
766        }
767    
768        /** 
769         * Compute the bounds of all of the Split/Divider/Leaf Nodes in 
770         * the layout model, and then set the bounds of each child component
771         * with a matching Leaf Node.
772         */
773        public void layoutContainer(Container parent) {
774        checkLayout(getModel());
775        Insets insets = parent.getInsets();
776        Dimension size = parent.getSize();
777        int width = size.width - (insets.left + insets.right);
778        int height = size.height - (insets.top + insets.bottom);
779        Rectangle bounds = new Rectangle(insets.left, insets.top, width, height);
780        layout1(getModel(), bounds); 
781        layout2(getModel(), bounds); 
782        }
783    
784    
785        private Divider dividerAt(Node root, int x, int y) {
786        if (root instanceof Divider) {
787                Divider divider = (Divider)root;
788            return (divider.getBounds().contains(x, y)) ? divider : null;
789        }
790        else if (root instanceof Split) {
791            Split split = (Split)root;
792            for(Node child : split.getChildren()) {
793            if (child.getBounds().contains(x, y)) {
794                return dividerAt(child, x, y);
795            }
796            }
797        }
798        return null;
799        }
800    
801        /** 
802         * Return the Divider whose bounds contain the specified
803         * point, or null if there isn't one.
804         * 
805         * @param x x coordinate
806         * @param y y coordinate
807         * @return the Divider at x,y
808         */
809        public Divider dividerAt(int x, int y) {
810        return dividerAt(getModel(), x, y);
811        }
812    
813        private boolean nodeOverlapsRectangle(Node node, Rectangle r2) {
814        Rectangle r1 = node.getBounds();
815        return 
816            (r1.x <= (r2.x + r2.width)) && ((r1.x + r1.width) >= r2.x) &&
817            (r1.y <= (r2.y + r2.height)) && ((r1.y + r1.height) >= r2.y);
818        }
819    
820        private List<Divider> dividersThatOverlap(Node root, Rectangle r) {
821        if (nodeOverlapsRectangle(root, r) && (root instanceof Split)) {
822            List<Divider> dividers = new ArrayList();
823            for(Node child : ((Split)root).getChildren()) {
824            if (child instanceof Divider) {
825                if (nodeOverlapsRectangle(child, r)) {
826                dividers.add((Divider)child);
827                }
828            }
829            else if (child instanceof Split) {
830                dividers.addAll(dividersThatOverlap(child, r));
831            }
832            }
833                return dividers;
834        }
835        else {
836            return Collections.emptyList();
837        }
838        }
839    
840        /**
841         * Return the Dividers whose bounds overlap the specified
842         * Rectangle.
843         * 
844         * @param r target Rectangle
845         * @return the Dividers that overlap r
846         * @throws IllegalArgumentException if the Rectangle is null
847         */
848        public List<Divider> dividersThatOverlap(Rectangle r) {
849        if (r == null) {
850            throw new IllegalArgumentException("null Rectangle");
851        }
852        return dividersThatOverlap(getModel(), r);
853        }
854    
855    
856        /** 
857         * Base class for the nodes that model a MultiSplitLayout.
858         */
859        public static abstract class Node {
860        private Split parent = null;  
861        private Rectangle bounds = new Rectangle();
862        private double weight = 0.0;
863    
864        /** 
865         * Returns the Split parent of this Node, or null.
866         *
867         * @return the value of the parent property.
868         * @see #setParent
869         */
870        public Split getParent() { return parent; }
871    
872        /**
873         * Set the value of this Node's parent property.  The default
874         * value of this property is null.
875         * 
876         * @param parent a Split or null
877         * @see #getParent
878         */
879        public void setParent(Split parent) {
880            this.parent = parent;
881        }
882        
883        /**
884         * Returns the bounding Rectangle for this Node.
885         * 
886         * @return the value of the bounds property.
887         * @see #setBounds
888         */
889        public Rectangle getBounds() { 
890            return new Rectangle(this.bounds);
891        }
892    
893        /**
894         * Set the bounding Rectangle for this node.  The value of 
895         * bounds may not be null.  The default value of bounds
896         * is equal to <code>new Rectangle(0,0,0,0)</code>.
897         * 
898         * @param bounds the new value of the bounds property
899         * @throws IllegalArgumentException if bounds is null
900         * @see #getBounds
901         */
902        public void setBounds(Rectangle bounds) {
903            if (bounds == null) {
904            throw new IllegalArgumentException("null bounds");
905            }
906            this.bounds = new Rectangle(bounds);
907        }
908    
909        /** 
910         * Value between 0.0 and 1.0 used to compute how much space
911         * to add to this sibling when the layout grows or how
912         * much to reduce when the layout shrinks.
913         * 
914         * @return the value of the weight property
915         * @see #setWeight
916         */
917        public double getWeight() { return weight; }
918    
919        /** 
920         * The weight property is a between 0.0 and 1.0 used to
921         * compute how much space to add to this sibling when the
922         * layout grows or how much to reduce when the layout shrinks.
923         * If rowLayout is true then this node's width grows
924         * or shrinks by (extraSpace * weight).  If rowLayout is false,
925         * then the node's height is changed.  The default value
926         * of weight is 0.0.
927         * 
928         * @param weight a double between 0.0 and 1.0
929         * @see #getWeight
930         * @see MultiSplitLayout#layoutContainer
931         * @throws IllegalArgumentException if weight is not between 0.0 and 1.0
932         */
933        public void setWeight(double weight) {
934            if ((weight < 0.0)|| (weight > 1.0)) {
935            throw new IllegalArgumentException("invalid weight");
936            }
937            this.weight = weight;
938        }
939    
940        private Node siblingAtOffset(int offset) {
941            Split parent = getParent();
942            if (parent == null) { return null; }
943            List<Node> siblings = parent.getChildren();
944            int index = siblings.indexOf(this);
945            if (index == -1) { return null; }
946            index += offset;
947            return ((index > -1) && (index < siblings.size())) ? siblings.get(index) : null;
948        }
949            
950        /** 
951         * Return the Node that comes after this one in the parent's
952         * list of children, or null.  If this node's parent is null,
953         * or if it's the last child, then return null.
954         * 
955         * @return the Node that comes after this one in the parent's list of children.
956         * @see #previousSibling
957         * @see #getParent
958         */
959        public Node nextSibling() {
960            return siblingAtOffset(+1);
961        }
962    
963        /** 
964         * Return the Node that comes before this one in the parent's
965         * list of children, or null.  If this node's parent is null,
966         * or if it's the last child, then return null.
967         * 
968         * @return the Node that comes before this one in the parent's list of children.
969         * @see #nextSibling
970         * @see #getParent
971         */
972        public Node previousSibling() {
973            return siblingAtOffset(-1);
974        }
975        }
976    
977        /** 
978         * Defines a vertical or horizontal subdivision into two or more
979         * tiles.
980         */
981        public static class Split extends Node {
982        private List<Node> children = Collections.emptyList();
983        private boolean rowLayout = true;
984    
985        /**
986         * Returns true if the this Split's children are to be 
987         * laid out in a row: all the same height, left edge
988         * equal to the previous Node's right edge.  If false,
989         * children are laid on in a column.
990         * 
991         * @return the value of the rowLayout property.
992         * @see #setRowLayout
993         */
994        public boolean isRowLayout() { return rowLayout; }
995    
996        /**
997         * Set the rowLayout property.  If true, all of this Split's
998         * children are to be laid out in a row: all the same height,
999         * each node's left edge equal to the previous Node's right
1000         * edge.  If false, children are laid on in a column.  Default
1001         * value is true.
1002         * 
1003         * @param rowLayout true for horizontal row layout, false for column
1004         * @see #isRowLayout
1005         */
1006        public void setRowLayout(boolean rowLayout) {
1007            this.rowLayout = rowLayout;
1008        }
1009    
1010        /** 
1011         * Returns this Split node's children.  The returned value
1012         * is not a reference to the Split's internal list of children
1013         * 
1014         * @return the value of the children property.
1015         * @see #setChildren
1016         */
1017        public List<Node> getChildren() { 
1018            return new ArrayList<Node>(children);
1019        }
1020    
1021        /**
1022         * Set's the children property of this Split node.  The parent
1023         * of each new child is set to this Split node, and the parent
1024         * of each old child (if any) is set to null.  This method
1025         * defensively copies the incoming List.  Default value is
1026         * an empty List.
1027         * 
1028         * @param children List of children
1029         * @see #getChildren
1030         * @throws IllegalArgumentException if children is null
1031         */
1032        public void setChildren(List<Node> children) {
1033            if (children == null) {
1034            throw new IllegalArgumentException("children must be a non-null List");
1035            }
1036            for(Node child : this.children) {
1037            child.setParent(null);
1038            }
1039            this.children = new ArrayList<Node>(children);
1040            for(Node child : this.children) {
1041            child.setParent(this);
1042            }
1043        }
1044    
1045        /**
1046         * Convenience method that returns the last child whose weight
1047         * is > 0.0.
1048         * 
1049         * @return the last child whose weight is > 0.0.
1050         * @see #getChildren
1051         * @see Node#getWeight
1052         */
1053        public final Node lastWeightedChild() {
1054            List<Node> children = getChildren();
1055            Node weightedChild = null;
1056            for(Node child : children) {
1057            if (child.getWeight() > 0.0) {
1058                weightedChild = child;
1059            }
1060            }
1061            return weightedChild;
1062        }
1063    
1064        public String toString() {
1065            int nChildren = getChildren().size();
1066            StringBuffer sb = new StringBuffer("MultiSplitLayout.Split");
1067            sb.append(isRowLayout() ? " ROW [" : " COLUMN [");
1068            sb.append(nChildren + ((nChildren == 1) ? " child" : " children"));
1069            sb.append("] ");
1070            sb.append(getBounds());
1071            return sb.toString();
1072        }
1073        }
1074    
1075    
1076        /**
1077         * Models a java.awt Component child.
1078         */
1079        public static class Leaf extends Node {
1080        private String name = "";
1081    
1082        /**
1083         * Create a Leaf node.  The default value of name is "". 
1084         */
1085        public Leaf() { }
1086    
1087        /**
1088         * Create a Leaf node with the specified name.  Name can not
1089         * be null.
1090         * 
1091         * @param name value of the Leaf's name property
1092         * @throws IllegalArgumentException if name is null
1093         */
1094        public Leaf(String name) {
1095            if (name == null) {
1096            throw new IllegalArgumentException("name is null");
1097            }
1098            this.name = name;
1099        }
1100    
1101        /**
1102         * Return the Leaf's name.
1103         * 
1104         * @return the value of the name property.
1105         * @see #setName
1106         */
1107        public String getName() { return name; }
1108    
1109        /**
1110         * Set the value of the name property.  Name may not be null.
1111         * 
1112         * @param name value of the name property
1113         * @throws IllegalArgumentException if name is null
1114         */
1115        public void setName(String name) {
1116            if (name == null) {
1117            throw new IllegalArgumentException("name is null");
1118            }
1119            this.name = name;
1120        }
1121    
1122        public String toString() {
1123            StringBuffer sb = new StringBuffer("MultiSplitLayout.Leaf");
1124            sb.append(" \"");
1125            sb.append(getName());
1126            sb.append("\""); 
1127            sb.append(" weight=");
1128            sb.append(getWeight());
1129            sb.append(" ");
1130            sb.append(getBounds());
1131            return sb.toString();
1132        }
1133        }
1134    
1135    
1136        /** 
1137         * Models a single vertical/horiztonal divider.
1138         */
1139        public static class Divider extends Node {
1140        /**
1141         * Convenience method, returns true if the Divider's parent
1142         * is a Split row (a Split with isRowLayout() true), false
1143         * otherwise. In other words if this Divider's major axis
1144         * is vertical, return true.
1145         * 
1146         * @return true if this Divider is part of a Split row.
1147         */
1148        public final boolean isVertical() {
1149            Split parent = getParent();
1150            return (parent != null) ? parent.isRowLayout() : false;
1151        }
1152    
1153        /** 
1154         * Dividers can't have a weight, they don't grow or shrink.
1155         * @throws UnsupportedOperationException
1156         */
1157        public void setWeight(double weight) {
1158            throw new UnsupportedOperationException();
1159        }
1160    
1161        public String toString() {
1162            return "MultiSplitLayout.Divider " + getBounds().toString();
1163        }
1164        }
1165    
1166    
1167        private static void throwParseException(StreamTokenizer st, String msg) throws Exception {
1168        throw new Exception("MultiSplitLayout.parseModel Error: " + msg);
1169        }
1170    
1171        private static void parseAttribute(String name, StreamTokenizer st, Node node) throws Exception {
1172        if ((st.nextToken() != '=')) {
1173            throwParseException(st, "expected '=' after " + name);
1174        }
1175        if (name.equalsIgnoreCase("WEIGHT")) {
1176            if (st.nextToken() == StreamTokenizer.TT_NUMBER) {
1177            node.setWeight(st.nval);
1178            }
1179            else { 
1180            throwParseException(st, "invalid weight");
1181            }
1182        }
1183        else if (name.equalsIgnoreCase("NAME")) {
1184            if (st.nextToken() == StreamTokenizer.TT_WORD) {
1185            if (node instanceof Leaf) {
1186                ((Leaf)node).setName(st.sval);
1187            }
1188            else {
1189                throwParseException(st, "can't specify name for " + node);
1190            }
1191            }
1192            else {
1193            throwParseException(st, "invalid name");
1194            }
1195        }
1196        else {
1197            throwParseException(st, "unrecognized attribute \"" + name + "\"");
1198        }
1199        }
1200    
1201        private static void addSplitChild(Split parent, Node child) {
1202        List<Node> children = new ArrayList(parent.getChildren());
1203        if (children.size() == 0) {
1204            children.add(child);
1205        }
1206        else {
1207            children.add(new Divider());
1208            children.add(child);
1209        }
1210        parent.setChildren(children);
1211        }
1212    
1213        private static void parseLeaf(StreamTokenizer st, Split parent) throws Exception {
1214        Leaf leaf = new Leaf();
1215        int token;
1216        while ((token = st.nextToken()) != StreamTokenizer.TT_EOF) {
1217            if (token == ')') {
1218            break;
1219            }
1220            if (token == StreamTokenizer.TT_WORD) {
1221            parseAttribute(st.sval, st, leaf);
1222            }
1223            else {
1224            throwParseException(st, "Bad Leaf: " + leaf);
1225            }
1226        }
1227        addSplitChild(parent, leaf);
1228        }
1229    
1230        private static void parseSplit(StreamTokenizer st, Split parent) throws Exception {
1231        int token;
1232        while ((token = st.nextToken()) != StreamTokenizer.TT_EOF) {
1233            if (token == ')') {
1234            break;
1235            }
1236            else if (token == StreamTokenizer.TT_WORD) {
1237            if (st.sval.equalsIgnoreCase("WEIGHT")) {
1238                parseAttribute(st.sval, st, parent);
1239            }
1240            else {
1241                addSplitChild(parent, new Leaf(st.sval));
1242            }
1243            }
1244            else if (token == '(') {
1245            if ((token = st.nextToken()) != StreamTokenizer.TT_WORD) {
1246                throwParseException(st, "invalid node type");
1247            }
1248            String nodeType = st.sval.toUpperCase();
1249            if (nodeType.equals("LEAF")) {
1250                parseLeaf(st, parent);
1251            }
1252            else if (nodeType.equals("ROW") || nodeType.equals("COLUMN")) {
1253                Split split = new Split();
1254                split.setRowLayout(nodeType.equals("ROW"));
1255                addSplitChild(parent, split);
1256                parseSplit(st, split);
1257            }
1258            else {
1259                throwParseException(st, "unrecognized node type '" + nodeType + "'");
1260            }
1261            }
1262        }
1263        }
1264    
1265        private static Node parseModel (Reader r) {
1266        StreamTokenizer st = new StreamTokenizer(r);
1267        try {
1268            Split root = new Split();
1269            parseSplit(st, root);
1270            return root.getChildren().get(0);
1271        }
1272        catch (Exception e) {
1273            System.err.println(e);
1274        }
1275        finally {
1276            try { r.close(); } catch (IOException ignore) {}
1277        }
1278        return null;
1279        }
1280    
1281        /**
1282         * A convenience method that converts a string to a 
1283         * MultiSplitLayout model (a tree of Nodes) using a 
1284         * a simple syntax.  Nodes are represented by 
1285         * parenthetical expressions whose first token 
1286         * is one of ROW/COLUMN/LEAF.  ROW and COLUMN specify
1287         * horizontal and vertical Split nodes respectively, 
1288         * LEAF specifies a Leaf node.  A Leaf's name and 
1289         * weight can be specified with attributes, 
1290         * name=<i>myLeafName</i> weight=<i>myLeafWeight</i>.
1291         * Similarly, a Split's weight can be specified with
1292         * weight=<i>mySplitWeight</i>.
1293         * 
1294         * <p> For example, the following expression generates
1295         * a horizontal Split node with three children:
1296         * the Leafs named left and right, and a Divider in 
1297         * between:
1298         * <pre>
1299         * (ROW (LEAF name=left) (LEAF name=right weight=1.0))
1300         * </pre>
1301         * 
1302         * <p> Dividers should not be included in the string, 
1303         * they're added automatcially as needed.  Because 
1304         * Leaf nodes often only need to specify a name, one
1305         * can specify a Leaf by just providing the name.
1306         * The previous example can be written like this:
1307         * <pre>
1308         * (ROW left (LEAF name=right weight=1.0))
1309         * </pre>
1310         * 
1311         * <p>Here's a more complex example.  One row with
1312         * three elements, the first and last of which are columns
1313         * with two leaves each:
1314         * <pre>
1315         * (ROW (COLUMN weight=0.5 left.top left.bottom) 
1316         *      (LEAF name=middle)
1317         *      (COLUMN weight=0.5 right.top right.bottom))
1318         * </pre>
1319         * 
1320         * 
1321         * <p> This syntax is not intended for archiving or 
1322         * configuration files .  It's just a convenience for
1323         * examples and tests.
1324         * 
1325         * @return the Node root of a tree based on s.
1326         */
1327        public static Node parseModel(String s) {
1328        return parseModel(new StringReader(s));
1329        }
1330    
1331    
1332        private static void printModel(String indent, Node root) {
1333        if (root instanceof Split) {
1334            Split split = (Split)root;
1335            System.out.println(indent + split);
1336            for(Node child : split.getChildren()) {
1337            printModel(indent + "  ", child);
1338            }
1339        }
1340        else {
1341            System.out.println(indent + root);
1342        }
1343        }
1344    
1345        /** 
1346         * Print the tree with enough detail for simple debugging.
1347         */
1348        public static void printModel(Node root) {
1349        printModel("", root);
1350        }
1351    }