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        }
248    }
249
250    private Component childForNode(Node node) {
251        if (node instanceof Leaf) {
252            Leaf leaf = (Leaf) node;
253            String name = leaf.getName();
254            return (name != null) ? childMap.get(name) : null;
255        }
256        return null;
257    }
258
259    private Dimension preferredComponentSize(Node node) {
260        Component child = childForNode(node);
261        return (child != null) ? child.getPreferredSize() : new Dimension(0, 0);
262
263    }
264
265    private Dimension preferredNodeSize(Node root) {
266        if (root instanceof Leaf)
267            return preferredComponentSize(root);
268        else if (root instanceof Divider) {
269            int dividerSize = getDividerSize();
270            return new Dimension(dividerSize, dividerSize);
271        } else {
272            Split split = (Split) root;
273            List<Node> splitChildren = split.getChildren();
274            int width = 0;
275            int height = 0;
276            if (split.isRowLayout()) {
277                for (Node splitChild : splitChildren) {
278                    Dimension size = preferredNodeSize(splitChild);
279                    width += size.width;
280                    height = Math.max(height, size.height);
281                }
282            } else {
283                for (Node splitChild : splitChildren) {
284                    Dimension size = preferredNodeSize(splitChild);
285                    width = Math.max(width, size.width);
286                    height += size.height;
287                }
288            }
289            return new Dimension(width, height);
290        }
291    }
292
293    private Dimension minimumNodeSize(Node root) {
294        if (root instanceof Leaf) {
295            Component child = childForNode(root);
296            return (child != null) ? child.getMinimumSize() : new Dimension(0, 0);
297        } else if (root instanceof Divider) {
298            int dividerSize = getDividerSize();
299            return new Dimension(dividerSize, dividerSize);
300        } else {
301            Split split = (Split) root;
302            List<Node> splitChildren = split.getChildren();
303            int width = 0;
304            int height = 0;
305            if (split.isRowLayout()) {
306                for (Node splitChild : splitChildren) {
307                    Dimension size = minimumNodeSize(splitChild);
308                    width += size.width;
309                    height = Math.max(height, size.height);
310                }
311            } else {
312                for (Node splitChild : splitChildren) {
313                    Dimension size = minimumNodeSize(splitChild);
314                    width = Math.max(width, size.width);
315                    height += size.height;
316                }
317            }
318            return new Dimension(width, height);
319        }
320    }
321
322    private static Dimension sizeWithInsets(Container parent, Dimension size) {
323        Insets insets = parent.getInsets();
324        int width = size.width + insets.left + insets.right;
325        int height = size.height + insets.top + insets.bottom;
326        return new Dimension(width, height);
327    }
328
329    @Override
330    public Dimension preferredLayoutSize(Container parent) {
331        Dimension size = preferredNodeSize(getModel());
332        return sizeWithInsets(parent, size);
333    }
334
335    @Override
336    public Dimension minimumLayoutSize(Container parent) {
337        Dimension size = minimumNodeSize(getModel());
338        return sizeWithInsets(parent, size);
339    }
340
341    private static Rectangle boundsWithYandHeight(Rectangle bounds, double y, double height) {
342        Rectangle r = new Rectangle();
343        r.setBounds((int) (bounds.getX()), (int) y, (int) (bounds.getWidth()), (int) height);
344        return r;
345    }
346
347    private static Rectangle boundsWithXandWidth(Rectangle bounds, double x, double width) {
348        Rectangle r = new Rectangle();
349        r.setBounds((int) x, (int) (bounds.getY()), (int) width, (int) (bounds.getHeight()));
350        return r;
351    }
352
353    private static void minimizeSplitBounds(Split split, Rectangle bounds) {
354        Rectangle splitBounds = new Rectangle(bounds.x, bounds.y, 0, 0);
355        List<Node> splitChildren = split.getChildren();
356        Node lastChild = splitChildren.get(splitChildren.size() - 1);
357        Rectangle lastChildBounds = lastChild.getBounds();
358        if (split.isRowLayout()) {
359            int lastChildMaxX = lastChildBounds.x + lastChildBounds.width;
360            splitBounds.add(lastChildMaxX, bounds.y + bounds.height);
361        } else {
362            int lastChildMaxY = lastChildBounds.y + lastChildBounds.height;
363            splitBounds.add(bounds.x + bounds.width, lastChildMaxY);
364        }
365        split.setBounds(splitBounds);
366    }
367
368    private void layoutShrink(Split split, Rectangle bounds) {
369        Rectangle splitBounds = split.getBounds();
370        ListIterator<Node> splitChildren = split.getChildren().listIterator();
371
372        if (split.isRowLayout()) {
373            int totalWidth = 0;          // sum of the children's widths
374            int minWeightedWidth = 0;    // sum of the weighted childrens' min widths
375            int totalWeightedWidth = 0;  // sum of the weighted childrens' widths
376            for (Node splitChild : split.getChildren()) {
377                int nodeWidth = splitChild.getBounds().width;
378                int nodeMinWidth = Math.min(nodeWidth, minimumNodeSize(splitChild).width);
379                totalWidth += nodeWidth;
380                if (splitChild.getWeight() > 0.0) {
381                    minWeightedWidth += nodeMinWidth;
382                    totalWeightedWidth += nodeWidth;
383                }
384            }
385
386            double x = bounds.getX();
387            double extraWidth = splitBounds.getWidth() - bounds.getWidth();
388            double availableWidth = extraWidth;
389            boolean onlyShrinkWeightedComponents =
390                (totalWeightedWidth - minWeightedWidth) > extraWidth;
391
392            while (splitChildren.hasNext()) {
393                Node splitChild = splitChildren.next();
394                Rectangle splitChildBounds = splitChild.getBounds();
395                double minSplitChildWidth = minimumNodeSize(splitChild).getWidth();
396                double splitChildWeight = onlyShrinkWeightedComponents
397                ? splitChild.getWeight()
398                        : (splitChildBounds.getWidth() / totalWidth);
399
400                if (!splitChildren.hasNext()) {
401                    double newWidth =  Math.max(minSplitChildWidth, bounds.getMaxX() - x);
402                    Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
403                    layout2(splitChild, newSplitChildBounds);
404                } else if ((availableWidth > 0.0) && (splitChildWeight > 0.0)) {
405                    double allocatedWidth = Math.rint(splitChildWeight * extraWidth);
406                    double oldWidth = splitChildBounds.getWidth();
407                    double newWidth = Math.max(minSplitChildWidth, oldWidth - allocatedWidth);
408                    Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
409                    layout2(splitChild, newSplitChildBounds);
410                    availableWidth -= (oldWidth - splitChild.getBounds().getWidth());
411                } else {
412                    double existingWidth = splitChildBounds.getWidth();
413                    Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, existingWidth);
414                    layout2(splitChild, newSplitChildBounds);
415                }
416                x = splitChild.getBounds().getMaxX();
417            }
418        } else {
419            int totalHeight = 0;          // sum of the children's heights
420            int minWeightedHeight = 0;    // sum of the weighted childrens' min heights
421            int totalWeightedHeight = 0;  // sum of the weighted childrens' heights
422            for (Node splitChild : split.getChildren()) {
423                int nodeHeight = splitChild.getBounds().height;
424                int nodeMinHeight = Math.min(nodeHeight, minimumNodeSize(splitChild).height);
425                totalHeight += nodeHeight;
426                if (splitChild.getWeight() > 0.0) {
427                    minWeightedHeight += nodeMinHeight;
428                    totalWeightedHeight += nodeHeight;
429                }
430            }
431
432            double y = bounds.getY();
433            double extraHeight = splitBounds.getHeight() - bounds.getHeight();
434            double availableHeight = extraHeight;
435            boolean onlyShrinkWeightedComponents =
436                (totalWeightedHeight - minWeightedHeight) > extraHeight;
437
438            while (splitChildren.hasNext()) {
439                Node splitChild = splitChildren.next();
440                Rectangle splitChildBounds = splitChild.getBounds();
441                double minSplitChildHeight = minimumNodeSize(splitChild).getHeight();
442                double splitChildWeight = onlyShrinkWeightedComponents
443                ? splitChild.getWeight()
444                        : (splitChildBounds.getHeight() / totalHeight);
445
446                if (!splitChildren.hasNext()) {
447                    double oldHeight = splitChildBounds.getHeight();
448                    double newHeight =  Math.max(minSplitChildHeight, bounds.getMaxY() - y);
449                    Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
450                    layout2(splitChild, newSplitChildBounds);
451                    availableHeight -= (oldHeight - splitChild.getBounds().getHeight());
452                } else if ((availableHeight > 0.0) && (splitChildWeight > 0.0)) {
453                    double allocatedHeight = Math.rint(splitChildWeight * extraHeight);
454                    double oldHeight = splitChildBounds.getHeight();
455                    double newHeight = Math.max(minSplitChildHeight, oldHeight - allocatedHeight);
456                    Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
457                    layout2(splitChild, newSplitChildBounds);
458                    availableHeight -= (oldHeight - splitChild.getBounds().getHeight());
459                } else {
460                    double existingHeight = splitChildBounds.getHeight();
461                    Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, existingHeight);
462                    layout2(splitChild, newSplitChildBounds);
463                }
464                y = splitChild.getBounds().getMaxY();
465            }
466        }
467
468        /* The bounds of the Split node root are set to be
469         * big enough to contain all of its children. Since
470         * Leaf children can't be reduced below their
471         * (corresponding java.awt.Component) minimum sizes,
472         * the size of the Split's bounds maybe be larger than
473         * the bounds we were asked to fit within.
474         */
475        minimizeSplitBounds(split, bounds);
476    }
477
478    private void layoutGrow(Split split, Rectangle bounds) {
479        Rectangle splitBounds = split.getBounds();
480        ListIterator<Node> splitChildren = split.getChildren().listIterator();
481        Node lastWeightedChild = split.lastWeightedChild();
482
483        if (split.isRowLayout()) {
484            /* Layout the Split's child Nodes' along the X axis.  The bounds
485             * of each child will have the same y coordinate and height as the
486             * layoutGrow() bounds argument.  Extra width is allocated to the
487             * to each child with a non-zero weight:
488             *     newWidth = currentWidth + (extraWidth * splitChild.getWeight())
489             * Any extraWidth "left over" (that's availableWidth in the loop
490             * below) is given to the last child.  Note that Dividers always
491             * have a weight of zero, and they're never the last child.
492             */
493            double x = bounds.getX();
494            double extraWidth = bounds.getWidth() - splitBounds.getWidth();
495            double availableWidth = extraWidth;
496
497            while (splitChildren.hasNext()) {
498                Node splitChild = splitChildren.next();
499                Rectangle splitChildBounds = splitChild.getBounds();
500                double splitChildWeight = splitChild.getWeight();
501
502                if (!splitChildren.hasNext()) {
503                    double newWidth = bounds.getMaxX() - x;
504                    Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
505                    layout2(splitChild, newSplitChildBounds);
506                } else if ((availableWidth > 0.0) && (splitChildWeight > 0.0)) {
507                    double allocatedWidth = splitChild.equals(lastWeightedChild)
508                    ? availableWidth
509                            : Math.rint(splitChildWeight * extraWidth);
510                    double newWidth = splitChildBounds.getWidth() + allocatedWidth;
511                    Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
512                    layout2(splitChild, newSplitChildBounds);
513                    availableWidth -= allocatedWidth;
514                } else {
515                    double existingWidth = splitChildBounds.getWidth();
516                    Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, existingWidth);
517                    layout2(splitChild, newSplitChildBounds);
518                }
519                x = splitChild.getBounds().getMaxX();
520            }
521        } else {
522            /* Layout the Split's child Nodes' along the Y axis.  The bounds
523             * of each child will have the same x coordinate and width as the
524             * layoutGrow() bounds argument.  Extra height is allocated to the
525             * to each child with a non-zero weight:
526             *     newHeight = currentHeight + (extraHeight * splitChild.getWeight())
527             * Any extraHeight "left over" (that's availableHeight in the loop
528             * below) is given to the last child.  Note that Dividers always
529             * have a weight of zero, and they're never the last child.
530             */
531            double y = bounds.getY();
532            double extraHeight = bounds.getMaxY() - splitBounds.getHeight();
533            double availableHeight = extraHeight;
534
535            while (splitChildren.hasNext()) {
536                Node splitChild = splitChildren.next();
537                Rectangle splitChildBounds = splitChild.getBounds();
538                double splitChildWeight = splitChild.getWeight();
539
540                if (!splitChildren.hasNext()) {
541                    double newHeight = bounds.getMaxY() - y;
542                    Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
543                    layout2(splitChild, newSplitChildBounds);
544                } else if ((availableHeight > 0.0) && (splitChildWeight > 0.0)) {
545                    double allocatedHeight = splitChild.equals(lastWeightedChild)
546                    ? availableHeight
547                            : Math.rint(splitChildWeight * extraHeight);
548                    double newHeight = splitChildBounds.getHeight() + allocatedHeight;
549                    Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
550                    layout2(splitChild, newSplitChildBounds);
551                    availableHeight -= allocatedHeight;
552                } else {
553                    double existingHeight = splitChildBounds.getHeight();
554                    Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, existingHeight);
555                    layout2(splitChild, newSplitChildBounds);
556                }
557                y = splitChild.getBounds().getMaxY();
558            }
559        }
560    }
561
562    /* Second pass of the layout algorithm: branch to layoutGrow/Shrink
563     * as needed.
564     */
565    private void layout2(Node root, Rectangle bounds) {
566        if (root instanceof Leaf) {
567            Component child = childForNode(root);
568            if (child != null) {
569                child.setBounds(bounds);
570            }
571            root.setBounds(bounds);
572        } else if (root instanceof Divider) {
573            root.setBounds(bounds);
574        } else if (root instanceof Split) {
575            Split split = (Split) root;
576            boolean grow = split.isRowLayout()
577            ? split.getBounds().width <= bounds.width
578                    : (split.getBounds().height <= bounds.height);
579            if (grow) {
580                layoutGrow(split, bounds);
581                root.setBounds(bounds);
582            } else {
583                layoutShrink(split, bounds);
584                // split.setBounds() called in layoutShrink()
585            }
586        }
587    }
588
589    /* First pass of the layout algorithm.
590     *
591     * If the Dividers are "floating" then set the bounds of each
592     * node to accomodate the preferred size of all of the
593     * Leaf's java.awt.Components.  Otherwise, just set the bounds
594     * of each Leaf/Split node so that it's to the left of (for
595     * Split.isRowLayout() Split children) or directly above
596     * the Divider that follows.
597     *
598     * This pass sets the bounds of each Node in the layout model.  It
599     * does not resize any of the parent Container's
600     * (java.awt.Component) children.  That's done in the second pass,
601     * see layoutGrow() and layoutShrink().
602     */
603    private void layout1(Node root, Rectangle bounds) {
604        if (root instanceof Leaf) {
605            root.setBounds(bounds);
606        } else if (root instanceof Split) {
607            Split split = (Split) root;
608            Iterator<Node> splitChildren = split.getChildren().iterator();
609            Rectangle childBounds;
610            int dividerSize = getDividerSize();
611
612            /* Layout the Split's child Nodes' along the X axis.  The bounds
613             * of each child will have the same y coordinate and height as the
614             * layout1() bounds argument.
615             *
616             * Note: the column layout code - that's the "else" clause below
617             * this if, is identical to the X axis (rowLayout) code below.
618             */
619            if (split.isRowLayout()) {
620                double x = bounds.getX();
621                while (splitChildren.hasNext()) {
622                    Node splitChild = splitChildren.next();
623                    Divider dividerChild =
624                        splitChildren.hasNext() ? (Divider) (splitChildren.next()) : null;
625
626                    double childWidth;
627                    if (getFloatingDividers()) {
628                        childWidth = preferredNodeSize(splitChild).getWidth();
629                    } else {
630                        if (dividerChild != null) {
631                            childWidth = dividerChild.getBounds().getX() - x;
632                        } else {
633                            childWidth = split.getBounds().getMaxX() - x;
634                        }
635                    }
636                    childBounds = boundsWithXandWidth(bounds, x, childWidth);
637                    layout1(splitChild, childBounds);
638
639                    if (getFloatingDividers() && (dividerChild != null)) {
640                        double dividerX = childBounds.getMaxX();
641                        Rectangle dividerBounds = boundsWithXandWidth(bounds, dividerX, dividerSize);
642                        dividerChild.setBounds(dividerBounds);
643                    }
644                    if (dividerChild != null) {
645                        x = dividerChild.getBounds().getMaxX();
646                    }
647                }
648            } else {
649                /* Layout the Split's child Nodes' along the Y axis.  The bounds
650                 * of each child will have the same x coordinate and width as the
651                 * layout1() bounds argument.  The algorithm is identical to what's
652                 * explained above, for the X axis case.
653                 */
654                double y = bounds.getY();
655                while (splitChildren.hasNext()) {
656                    Node splitChild = splitChildren.next();
657                    Divider dividerChild =
658                        splitChildren.hasNext() ? (Divider) splitChildren.next() : null;
659
660                        double childHeight;
661                        if (getFloatingDividers()) {
662                            childHeight = preferredNodeSize(splitChild).getHeight();
663                        } else {
664                            if (dividerChild != null) {
665                                childHeight = dividerChild.getBounds().getY() - y;
666                            } else {
667                                childHeight = split.getBounds().getMaxY() - y;
668                            }
669                        }
670                        childBounds = boundsWithYandHeight(bounds, y, childHeight);
671                        layout1(splitChild, childBounds);
672
673                        if (getFloatingDividers() && (dividerChild != null)) {
674                            double dividerY = childBounds.getMaxY();
675                            Rectangle dividerBounds = boundsWithYandHeight(bounds, dividerY, dividerSize);
676                            dividerChild.setBounds(dividerBounds);
677                        }
678                        if (dividerChild != null) {
679                            y = dividerChild.getBounds().getMaxY();
680                        }
681                }
682            }
683            /* The bounds of the Split node root are set to be just
684             * big enough to contain all of its children, but only
685             * along the axis it's allocating space on.  That's
686             * X for rows, Y for columns.  The second pass of the
687             * layout algorithm - see layoutShrink()/layoutGrow()
688             * allocates extra space.
689             */
690            minimizeSplitBounds(split, bounds);
691        }
692    }
693
694    /**
695     * The specified Node is either the wrong type or was configured incorrectly.
696     */
697    public static class InvalidLayoutException extends RuntimeException {
698        private final transient Node node;
699
700        /**
701         * Constructs a new {@code InvalidLayoutException}.
702         * @param msg the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
703         * @param node node
704         */
705        public InvalidLayoutException(String msg, Node node) {
706            super(msg);
707            this.node = node;
708        }
709
710        /**
711         * @return the invalid Node.
712         */
713        public Node getNode() {
714            return node;
715        }
716    }
717
718    private static void throwInvalidLayout(String msg, Node node) {
719        throw new InvalidLayoutException(msg, node);
720    }
721
722    private static void checkLayout(Node root) {
723        if (root instanceof Split) {
724            Split split = (Split) root;
725            if (split.getChildren().size() <= 2) {
726                throwInvalidLayout("Split must have > 2 children", root);
727            }
728            Iterator<Node> splitChildren = split.getChildren().iterator();
729            double weight = 0.0;
730            while (splitChildren.hasNext()) {
731                Node splitChild = splitChildren.next();
732                if (splitChild instanceof Divider) {
733                    throwInvalidLayout("expected a Split or Leaf Node", splitChild);
734                }
735                if (splitChildren.hasNext()) {
736                    Node dividerChild = splitChildren.next();
737                    if (!(dividerChild instanceof Divider)) {
738                        throwInvalidLayout("expected a Divider Node", dividerChild);
739                    }
740                }
741                weight += splitChild.getWeight();
742                checkLayout(splitChild);
743            }
744            if (weight > 1.0 + 0.000000001) { /* add some epsilon to a double check */
745                throwInvalidLayout("Split children's total weight > 1.0", root);
746            }
747        }
748    }
749
750    /**
751     * Compute the bounds of all of the Split/Divider/Leaf Nodes in
752     * the layout model, and then set the bounds of each child component
753     * with a matching Leaf Node.
754     */
755    @Override
756    public void layoutContainer(Container parent) {
757        checkLayout(getModel());
758        Insets insets = parent.getInsets();
759        Dimension size = parent.getSize();
760        int width = size.width - (insets.left + insets.right);
761        int height = size.height - (insets.top + insets.bottom);
762        Rectangle bounds = new Rectangle(insets.left, insets.top, width, height);
763        layout1(getModel(), bounds);
764        layout2(getModel(), bounds);
765    }
766
767    private static Divider dividerAt(Node root, int x, int y) {
768        if (root instanceof Divider) {
769            Divider divider = (Divider) root;
770            return divider.getBounds().contains(x, y) ? divider : null;
771        } else if (root instanceof Split) {
772            Split split = (Split) root;
773            for (Node child : split.getChildren()) {
774                if (child.getBounds().contains(x, y))
775                    return dividerAt(child, x, y);
776            }
777        }
778        return null;
779    }
780
781    /**
782     * Return the Divider whose bounds contain the specified
783     * point, or null if there isn't one.
784     *
785     * @param x x coordinate
786     * @param y y coordinate
787     * @return the Divider at x,y
788     */
789    public Divider dividerAt(int x, int y) {
790        return dividerAt(getModel(), x, y);
791    }
792
793    private static boolean nodeOverlapsRectangle(Node node, Rectangle r2) {
794        Rectangle r1 = node.getBounds();
795        return
796        (r1.x <= (r2.x + r2.width)) && ((r1.x + r1.width) >= r2.x) &&
797        (r1.y <= (r2.y + r2.height)) && ((r1.y + r1.height) >= r2.y);
798    }
799
800    private static List<Divider> dividersThatOverlap(Node root, Rectangle r) {
801        if (nodeOverlapsRectangle(root, r) && (root instanceof Split)) {
802            List<Divider> dividers = new ArrayList<>();
803            for (Node child : ((Split) root).getChildren()) {
804                if (child instanceof Divider) {
805                    if (nodeOverlapsRectangle(child, r)) {
806                        dividers.add((Divider) child);
807                    }
808                } else if (child instanceof Split) {
809                    dividers.addAll(dividersThatOverlap(child, r));
810                }
811            }
812            return dividers;
813        } else
814            return Collections.emptyList();
815    }
816
817    /**
818     * Return the Dividers whose bounds overlap the specified
819     * Rectangle.
820     *
821     * @param r target Rectangle
822     * @return the Dividers that overlap r
823     * @throws IllegalArgumentException if the Rectangle is null
824     */
825    public List<Divider> dividersThatOverlap(Rectangle r) {
826        CheckParameterUtil.ensureParameterNotNull(r, "r");
827        return dividersThatOverlap(getModel(), r);
828    }
829
830    /**
831     * Base class for the nodes that model a MultiSplitLayout.
832     */
833    public abstract static class Node {
834        private Split parent;
835        private Rectangle bounds = new Rectangle();
836        private double weight;
837
838        /**
839         * Returns the Split parent of this Node, or null.
840         *
841         * This method isn't called getParent(), in order to avoid problems
842         * with recursive object creation when using XmlDecoder.
843         *
844         * @return the value of the parent property.
845         * @see #parent_set
846         */
847        public Split parent_get() {
848            return parent;
849        }
850
851        /**
852         * Set the value of this Node's parent property.  The default
853         * value of this property is null.
854         *
855         * This method isn't called setParent(), in order to avoid problems
856         * with recursive object creation when using XmlEncoder.
857         *
858         * @param parent a Split or null
859         * @see #parent_get
860         */
861        public void parent_set(Split parent) {
862            this.parent = parent;
863        }
864
865        /**
866         * Returns the bounding Rectangle for this Node.
867         *
868         * @return the value of the bounds property.
869         * @see #setBounds
870         */
871        public Rectangle getBounds() {
872            return new Rectangle(this.bounds);
873        }
874
875        /**
876         * Set the bounding Rectangle for this node.  The value of
877         * bounds may not be null.  The default value of bounds
878         * is equal to <code>new Rectangle(0,0,0,0)</code>.
879         *
880         * @param bounds the new value of the bounds property
881         * @throws IllegalArgumentException if bounds is null
882         * @see #getBounds
883         */
884        public void setBounds(Rectangle bounds) {
885            CheckParameterUtil.ensureParameterNotNull(bounds, "bounds");
886            this.bounds = new Rectangle(bounds);
887        }
888
889        /**
890         * Value between 0.0 and 1.0 used to compute how much space
891         * to add to this sibling when the layout grows or how
892         * much to reduce when the layout shrinks.
893         *
894         * @return the value of the weight property
895         * @see #setWeight
896         */
897        public double getWeight() {
898            return weight;
899        }
900
901        /**
902         * The weight property is a between 0.0 and 1.0 used to
903         * compute how much space to add to this sibling when the
904         * layout grows or how much to reduce when the layout shrinks.
905         * If rowLayout is true then this node's width grows
906         * or shrinks by (extraSpace * weight).  If rowLayout is false,
907         * then the node's height is changed.  The default value
908         * of weight is 0.0.
909         *
910         * @param weight a double between 0.0 and 1.0
911         * @throws IllegalArgumentException if weight is not between 0.0 and 1.0
912         * @see #getWeight
913         * @see MultiSplitLayout#layoutContainer
914         */
915        public void setWeight(double weight) {
916            if ((weight < 0.0) || (weight > 1.0))
917                throw new IllegalArgumentException("invalid weight");
918            this.weight = weight;
919        }
920
921        private Node siblingAtOffset(int offset) {
922            Split parent = parent_get();
923            if (parent == null)
924                return null;
925            List<Node> siblings = parent.getChildren();
926            int index = siblings.indexOf(this);
927            if (index == -1)
928                return null;
929            index += offset;
930            return ((index > -1) && (index < siblings.size())) ? siblings.get(index) : null;
931        }
932
933        /**
934         * Return the Node that comes after 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 after this one in the parent's list of children.
939         * @see #previousSibling
940         * @see #parent_get
941         */
942        public Node nextSibling() {
943            return siblingAtOffset(+1);
944        }
945
946        /**
947         * Return the Node that comes before 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 before this one in the parent's list of children.
952         * @see #nextSibling
953         * @see #parent_get
954         */
955        public Node previousSibling() {
956            return siblingAtOffset(-1);
957        }
958    }
959
960    /**
961     * Defines a vertical or horizontal subdivision into two or more
962     * tiles.
963     */
964    public static class Split extends Node {
965        private List<Node> children = Collections.emptyList();
966        private boolean rowLayout = true;
967
968        /**
969         * Returns true if the this Split's children are to be
970         * laid out in a row: all the same height, left edge
971         * equal to the previous Node's right edge.  If false,
972         * children are laid on in a column.
973         *
974         * @return the value of the rowLayout property.
975         * @see #setRowLayout
976         */
977        public boolean isRowLayout() {
978            return rowLayout;
979        }
980
981        /**
982         * Set the rowLayout property.  If true, all of this Split's
983         * children are to be laid out in a row: all the same height,
984         * each node's left edge equal to the previous Node's right
985         * edge. If false, children are laid on in a column. Default value is true.
986         *
987         * @param rowLayout true for horizontal row layout, false for column
988         * @see #isRowLayout
989         */
990        public void setRowLayout(boolean rowLayout) {
991            this.rowLayout = rowLayout;
992        }
993
994        /**
995         * Returns this Split node's children.  The returned value
996         * is not a reference to the Split's internal list of children
997         *
998         * @return the value of the children property.
999         * @see #setChildren
1000         */
1001        public List<Node> getChildren() {
1002            return new ArrayList<>(children);
1003        }
1004
1005        /**
1006         * Set's the children property of this Split node.  The parent
1007         * of each new child is set to this Split node, and the parent
1008         * of each old child (if any) is set to null.  This method
1009         * defensively copies the incoming List. Default value is an empty List.
1010         *
1011         * @param children List of children
1012         * @throws IllegalArgumentException if children is null
1013         * @see #getChildren
1014         */
1015        public void setChildren(List<Node> children) {
1016            if (children == null)
1017                throw new IllegalArgumentException("children must be a non-null List");
1018            for (Node child : this.children) {
1019                child.parent_set(null);
1020            }
1021            this.children = new ArrayList<>(children);
1022            for (Node child : this.children) {
1023                child.parent_set(this);
1024            }
1025        }
1026
1027        /**
1028         * Convenience method that returns the last child whose weight
1029         * is &gt; 0.0.
1030         *
1031         * @return the last child whose weight is &gt; 0.0.
1032         * @see #getChildren
1033         * @see Node#getWeight
1034         */
1035        public final Node lastWeightedChild() {
1036            List<Node> children = getChildren();
1037            Node weightedChild = null;
1038            for (Node child : children) {
1039                if (child.getWeight() > 0.0) {
1040                    weightedChild = child;
1041                }
1042            }
1043            return weightedChild;
1044        }
1045
1046        @Override
1047        public String toString() {
1048            int nChildren = getChildren().size();
1049            StringBuilder sb = new StringBuilder("MultiSplitLayout.Split");
1050            sb.append(isRowLayout() ? " ROW [" : " COLUMN [")
1051              .append(nChildren + ((nChildren == 1) ? " child" : " children"))
1052              .append("] ")
1053              .append(getBounds());
1054            return sb.toString();
1055        }
1056    }
1057
1058    /**
1059     * Models a java.awt Component child.
1060     */
1061    public static class Leaf extends Node {
1062        private String name = "";
1063
1064        /**
1065         * Create a Leaf node. The default value of name is "".
1066         */
1067        public Leaf() {
1068            // Name can be set later with setName()
1069        }
1070
1071        /**
1072         * Create a Leaf node with the specified name. Name can not be null.
1073         *
1074         * @param name value of the Leaf's name property
1075         * @throws IllegalArgumentException if name is null
1076         */
1077        public Leaf(String name) {
1078            CheckParameterUtil.ensureParameterNotNull(name, "name");
1079            this.name = name;
1080        }
1081
1082        /**
1083         * Return the Leaf's name.
1084         *
1085         * @return the value of the name property.
1086         * @see #setName
1087         */
1088        public String getName() {
1089            return name;
1090        }
1091
1092        /**
1093         * Set the value of the name property.  Name may not be null.
1094         *
1095         * @param name value of the name property
1096         * @throws IllegalArgumentException if name is null
1097         */
1098        public void setName(String name) {
1099            CheckParameterUtil.ensureParameterNotNull(name, "name");
1100            this.name = name;
1101        }
1102
1103        @Override
1104        public String toString() {
1105            return new StringBuilder("MultiSplitLayout.Leaf \"")
1106              .append(getName())
1107              .append("\" weight=")
1108              .append(getWeight())
1109              .append(' ')
1110              .append(getBounds())
1111              .toString();
1112        }
1113    }
1114
1115    /**
1116     * Models a single vertical/horiztonal divider.
1117     */
1118    public static class Divider extends Node {
1119        /**
1120         * Convenience method, returns true if the Divider's parent
1121         * is a Split row (a Split with isRowLayout() true), false
1122         * otherwise. In other words if this Divider's major axis
1123         * is vertical, return true.
1124         *
1125         * @return true if this Divider is part of a Split row.
1126         */
1127        public final boolean isVertical() {
1128            Split parent = parent_get();
1129            return parent != null && parent.isRowLayout();
1130        }
1131
1132        /**
1133         * Dividers can't have a weight, they don't grow or shrink.
1134         * @throws UnsupportedOperationException always
1135         */
1136        @Override
1137        public void setWeight(double weight) {
1138            throw new UnsupportedOperationException();
1139        }
1140
1141        @Override
1142        public String toString() {
1143            return "MultiSplitLayout.Divider " + getBounds();
1144        }
1145    }
1146}