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