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