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