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