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