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