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