source: josm/trunk/src/org/openstreetmap/josm/gui/MultiSplitLayout.java @ 5241

Revision 3095, 50.8 KB checked in by jttt, 2 years ago (diff)

Changes in multipolygon handling (see #4661), cosmetics

  • Property svn:eol-style set to native
Line 
1/*
2 * $Id: MultiSplitLayout.java,v 1.15 2005/10/26 14:29:54 hansmuller Exp $
3 *
4 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
5 * Santa Clara, California 95054, U.S.A. All rights reserved.
6 *
7 * This library is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Lesser General Public
9 * License as published by the Free Software Foundation; either
10 * version 2.1 of the License, or (at your option) any later version.
11 *
12 * This library is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 * Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public
18 * License along with this library; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
20 */
21
22//package org.jdesktop.swingx;
23package org.openstreetmap.josm.gui;
24
25import java.awt.Component;
26import java.awt.Container;
27import java.awt.Dimension;
28import java.awt.Insets;
29import java.awt.LayoutManager;
30import java.awt.Rectangle;
31import java.beans.PropertyChangeListener;
32import java.beans.PropertyChangeSupport;
33import java.io.IOException;
34import java.io.Reader;
35import java.io.StreamTokenizer;
36import java.io.StringReader;
37import java.util.ArrayList;
38import java.util.Collections;
39import java.util.HashMap;
40import java.util.Iterator;
41import java.util.List;
42import java.util.ListIterator;
43import java.util.Map;
44
45import javax.swing.UIManager;
46
47/**
48 * The MultiSplitLayout layout manager recursively arranges its
49 * components in row and column groups called "Splits".  Elements of
50 * the layout are separated by gaps called "Dividers".  The overall
51 * layout is defined with a simple tree model whose nodes are
52 * instances of MultiSplitLayout.Split, MultiSplitLayout.Divider,
53 * and MultiSplitLayout.Leaf. Named Leaf nodes represent the space
54 * allocated to a component that was added with a constraint that
55 * matches the Leaf's name.  Extra space is distributed
56 * among row/column siblings according to their 0.0 to 1.0 weight.
57 * If no weights are specified then the last sibling always gets
58 * all of the extra space, or space reduction.
59 *
60 * <p>
61 * Although MultiSplitLayout can be used with any Container, it's
62 * the default layout manager for MultiSplitPane.  MultiSplitPane
63 * supports interactively dragging the Dividers, accessibility,
64 * and other features associated with split panes.
65 *
66 * <p>
67 * All properties in this class are bound: when a properties value
68 * is changed, all PropertyChangeListeners are fired.
69 *
70 * @author Hans Muller
71 * @see MultiSplitPane
72 */
73
74public class MultiSplitLayout implements LayoutManager {
75    private final Map<String, Component> childMap = new HashMap<String, Component>();
76    private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
77    private Node model;
78    private int dividerSize;
79    private boolean floatingDividers = true;
80
81    /**
82     * Create a MultiSplitLayout with a default model with a single
83     * Leaf node named "default".
84     *
85     * #see setModel
86     */
87    public MultiSplitLayout() {
88        this(new Leaf("default"));
89    }
90
91    /**
92     * Create a MultiSplitLayout with the specified model.
93     *
94     * #see setModel
95     */
96    public MultiSplitLayout(Node model) {
97        this.model = model;
98        this.dividerSize = UIManager.getInt("SplitPane.dividerSize");
99        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 < 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    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    public void removeLayoutComponent(Component child) {
225        String name = child.getName();
226        if (name != null) {
227            childMap.remove(name);
228        }
229    }
230
231    private Component childForNode(Node node) {
232        if (node instanceof Leaf) {
233            Leaf leaf = (Leaf)node;
234            String name = leaf.getName();
235            return (name != null) ? childMap.get(name) : null;
236        }
237        return null;
238    }
239
240    private Dimension preferredComponentSize(Node node) {
241        Component child = childForNode(node);
242        return (child != null) ? child.getPreferredSize() : new Dimension(0, 0);
243
244    }
245
246    private Dimension minimumComponentSize(Node node) {
247        Component child = childForNode(node);
248        return (child != null) ? child.getMinimumSize() : new Dimension(0, 0);
249
250    }
251
252    private Dimension preferredNodeSize(Node root) {
253        if (root instanceof Leaf)
254            return preferredComponentSize(root);
255        else if (root instanceof Divider) {
256            int dividerSize = getDividerSize();
257            return new Dimension(dividerSize, dividerSize);
258        }
259        else {
260            Split split = (Split)root;
261            List<Node> splitChildren = split.getChildren();
262            int width = 0;
263            int height = 0;
264            if (split.isRowLayout()) {
265                for(Node splitChild : splitChildren) {
266                    Dimension size = preferredNodeSize(splitChild);
267                    width += size.width;
268                    height = Math.max(height, size.height);
269                }
270            }
271            else {
272                for(Node splitChild : splitChildren) {
273                    Dimension size = preferredNodeSize(splitChild);
274                    width = Math.max(width, size.width);
275                    height += size.height;
276                }
277            }
278            return new Dimension(width, height);
279        }
280    }
281
282    private Dimension minimumNodeSize(Node root) {
283        if (root instanceof Leaf) {
284            Component child = childForNode(root);
285            return (child != null) ? child.getMinimumSize() : new Dimension(0, 0);
286        }
287        else if (root instanceof Divider) {
288            int dividerSize = getDividerSize();
289            return new Dimension(dividerSize, dividerSize);
290        }
291        else {
292            Split split = (Split)root;
293            List<Node> splitChildren = split.getChildren();
294            int width = 0;
295            int height = 0;
296            if (split.isRowLayout()) {
297                for(Node splitChild : splitChildren) {
298                    Dimension size = minimumNodeSize(splitChild);
299                    width += size.width;
300                    height = Math.max(height, size.height);
301                }
302            }
303            else {
304                for(Node splitChild : splitChildren) {
305                    Dimension size = minimumNodeSize(splitChild);
306                    width = Math.max(width, size.width);
307                    height += size.height;
308                }
309            }
310            return new Dimension(width, height);
311        }
312    }
313
314    private Dimension sizeWithInsets(Container parent, Dimension size) {
315        Insets insets = parent.getInsets();
316        int width = size.width + insets.left + insets.right;
317        int height = size.height + insets.top + insets.bottom;
318        return new Dimension(width, height);
319    }
320
321    public Dimension preferredLayoutSize(Container parent) {
322        Dimension size = preferredNodeSize(getModel());
323        return sizeWithInsets(parent, size);
324    }
325
326    public Dimension minimumLayoutSize(Container parent) {
327        Dimension size = minimumNodeSize(getModel());
328        return sizeWithInsets(parent, size);
329    }
330
331    private Rectangle boundsWithYandHeight(Rectangle bounds, double y, double height) {
332        Rectangle r = new Rectangle();
333        r.setBounds((int)(bounds.getX()), (int)y, (int)(bounds.getWidth()), (int)height);
334        return r;
335    }
336
337    private Rectangle boundsWithXandWidth(Rectangle bounds, double x, double width) {
338        Rectangle r = new Rectangle();
339        r.setBounds((int)x, (int)(bounds.getY()), (int)width, (int)(bounds.getHeight()));
340        return r;
341    }
342
343    private void minimizeSplitBounds(Split split, Rectangle bounds) {
344        Rectangle splitBounds = new Rectangle(bounds.x, bounds.y, 0, 0);
345        List<Node> splitChildren = split.getChildren();
346        Node lastChild = splitChildren.get(splitChildren.size() - 1);
347        Rectangle lastChildBounds = lastChild.getBounds();
348        if (split.isRowLayout()) {
349            int lastChildMaxX = lastChildBounds.x + lastChildBounds.width;
350            splitBounds.add(lastChildMaxX, bounds.y + bounds.height);
351        }
352        else {
353            int lastChildMaxY = lastChildBounds.y + lastChildBounds.height;
354            splitBounds.add(bounds.x + bounds.width, lastChildMaxY);
355        }
356        split.setBounds(splitBounds);
357    }
358
359    private void layoutShrink(Split split, Rectangle bounds) {
360        Rectangle splitBounds = split.getBounds();
361        ListIterator<Node> splitChildren = split.getChildren().listIterator();
362
363        if (split.isRowLayout()) {
364            int totalWidth = 0;          // sum of the children's widths
365            int minWeightedWidth = 0;    // sum of the weighted childrens' min widths
366            int totalWeightedWidth = 0;  // sum of the weighted childrens' widths
367            for(Node splitChild : split.getChildren()) {
368                int nodeWidth = splitChild.getBounds().width;
369                int nodeMinWidth = Math.min(nodeWidth, minimumNodeSize(splitChild).width);
370                totalWidth += nodeWidth;
371                if (splitChild.getWeight() > 0.0) {
372                    minWeightedWidth += nodeMinWidth;
373                    totalWeightedWidth += nodeWidth;
374                }
375            }
376
377            double x = bounds.getX();
378            double extraWidth = splitBounds.getWidth() - bounds.getWidth();
379            double availableWidth = extraWidth;
380            boolean onlyShrinkWeightedComponents =
381                (totalWeightedWidth - minWeightedWidth) > extraWidth;
382
383                while(splitChildren.hasNext()) {
384                    Node splitChild = splitChildren.next();
385                    Rectangle splitChildBounds = splitChild.getBounds();
386                    double minSplitChildWidth = minimumNodeSize(splitChild).getWidth();
387                    double splitChildWeight = (onlyShrinkWeightedComponents)
388                    ? splitChild.getWeight()
389                            : (splitChildBounds.getWidth() / totalWidth);
390
391                    if (!splitChildren.hasNext()) {
392                        double newWidth =  Math.max(minSplitChildWidth, bounds.getMaxX() - x);
393                        Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
394                        layout2(splitChild, newSplitChildBounds);
395                    }
396                    else if ((availableWidth > 0.0) && (splitChildWeight > 0.0)) {
397                        double allocatedWidth = Math.rint(splitChildWeight * extraWidth);
398                        double oldWidth = splitChildBounds.getWidth();
399                        double newWidth = Math.max(minSplitChildWidth, oldWidth - allocatedWidth);
400                        Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
401                        layout2(splitChild, newSplitChildBounds);
402                        availableWidth -= (oldWidth - splitChild.getBounds().getWidth());
403                    }
404                    else {
405                        double existingWidth = splitChildBounds.getWidth();
406                        Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, existingWidth);
407                        layout2(splitChild, newSplitChildBounds);
408                    }
409                    x = splitChild.getBounds().getMaxX();
410                }
411        }
412
413        else {
414            int totalHeight = 0;          // sum of the children's heights
415            int minWeightedHeight = 0;    // sum of the weighted childrens' min heights
416            int totalWeightedHeight = 0;  // sum of the weighted childrens' heights
417            for(Node splitChild : split.getChildren()) {
418                int nodeHeight = splitChild.getBounds().height;
419                int nodeMinHeight = Math.min(nodeHeight, minimumNodeSize(splitChild).height);
420                totalHeight += nodeHeight;
421                if (splitChild.getWeight() > 0.0) {
422                    minWeightedHeight += nodeMinHeight;
423                    totalWeightedHeight += nodeHeight;
424                }
425            }
426
427            double y = bounds.getY();
428            double extraHeight = splitBounds.getHeight() - bounds.getHeight();
429            double availableHeight = extraHeight;
430            boolean onlyShrinkWeightedComponents =
431                (totalWeightedHeight - minWeightedHeight) > extraHeight;
432
433                while(splitChildren.hasNext()) {
434                    Node splitChild = splitChildren.next();
435                    Rectangle splitChildBounds = splitChild.getBounds();
436                    double minSplitChildHeight = minimumNodeSize(splitChild).getHeight();
437                    double splitChildWeight = (onlyShrinkWeightedComponents)
438                    ? splitChild.getWeight()
439                            : (splitChildBounds.getHeight() / totalHeight);
440
441                    if (!splitChildren.hasNext()) {
442                        double oldHeight = splitChildBounds.getHeight();
443                        double newHeight =  Math.max(minSplitChildHeight, bounds.getMaxY() - y);
444                        Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
445                        layout2(splitChild, newSplitChildBounds);
446                        availableHeight -= (oldHeight - splitChild.getBounds().getHeight());
447                    }
448                    else if ((availableHeight > 0.0) && (splitChildWeight > 0.0)) {
449                        double allocatedHeight = Math.rint(splitChildWeight * extraHeight);
450                        double oldHeight = splitChildBounds.getHeight();
451                        double newHeight = Math.max(minSplitChildHeight, oldHeight - allocatedHeight);
452                        Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
453                        layout2(splitChild, newSplitChildBounds);
454                        availableHeight -= (oldHeight - splitChild.getBounds().getHeight());
455                    }
456                    else {
457                        double existingHeight = splitChildBounds.getHeight();
458                        Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, existingHeight);
459                        layout2(splitChild, newSplitChildBounds);
460                    }
461                    y = splitChild.getBounds().getMaxY();
462                }
463        }
464
465        /* The bounds of the Split node root are set to be
466         * big enough to contain all of its children. Since
467         * Leaf children can't be reduced below their
468         * (corresponding java.awt.Component) minimum sizes,
469         * the size of the Split's bounds maybe be larger than
470         * the bounds we were asked to fit within.
471         */
472        minimizeSplitBounds(split, bounds);
473    }
474
475    private void layoutGrow(Split split, Rectangle bounds) {
476        Rectangle splitBounds = split.getBounds();
477        ListIterator<Node> splitChildren = split.getChildren().listIterator();
478        Node lastWeightedChild = split.lastWeightedChild();
479
480        /* Layout the Split's child Nodes' along the X axis.  The bounds
481         * of each child will have the same y coordinate and height as the
482         * layoutGrow() bounds argument.  Extra width is allocated to the
483         * to each child with a non-zero weight:
484         *     newWidth = currentWidth + (extraWidth * splitChild.getWeight())
485         * Any extraWidth "left over" (that's availableWidth in the loop
486         * below) is given to the last child.  Note that Dividers always
487         * have a weight of zero, and they're never the last child.
488         */
489        if (split.isRowLayout()) {
490            double x = bounds.getX();
491            double extraWidth = bounds.getWidth() - splitBounds.getWidth();
492            double availableWidth = extraWidth;
493
494            while(splitChildren.hasNext()) {
495                Node splitChild = splitChildren.next();
496                Rectangle splitChildBounds = splitChild.getBounds();
497                double splitChildWeight = splitChild.getWeight();
498
499                if (!splitChildren.hasNext()) {
500                    double newWidth = bounds.getMaxX() - x;
501                    Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
502                    layout2(splitChild, newSplitChildBounds);
503                }
504                else if ((availableWidth > 0.0) && (splitChildWeight > 0.0)) {
505                    double allocatedWidth = (splitChild.equals(lastWeightedChild))
506                    ? availableWidth
507                            : Math.rint(splitChildWeight * extraWidth);
508                    double newWidth = splitChildBounds.getWidth() + allocatedWidth;
509                    Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
510                    layout2(splitChild, newSplitChildBounds);
511                    availableWidth -= allocatedWidth;
512                }
513                else {
514                    double existingWidth = splitChildBounds.getWidth();
515                    Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, existingWidth);
516                    layout2(splitChild, newSplitChildBounds);
517                }
518                x = splitChild.getBounds().getMaxX();
519            }
520        }
521
522        /* Layout the Split's child Nodes' along the Y axis.  The bounds
523         * of each child will have the same x coordinate and width as the
524         * layoutGrow() bounds argument.  Extra height is allocated to the
525         * to each child with a non-zero weight:
526         *     newHeight = currentHeight + (extraHeight * splitChild.getWeight())
527         * Any extraHeight "left over" (that's availableHeight in the loop
528         * below) is given to the last child.  Note that Dividers always
529         * have a weight of zero, and they're never the last child.
530         */
531        else {
532            double y = bounds.getY();
533            double extraHeight = bounds.getMaxY() - splitBounds.getHeight();
534            double availableHeight = extraHeight;
535
536            while(splitChildren.hasNext()) {
537                Node splitChild = splitChildren.next();
538                Rectangle splitChildBounds = splitChild.getBounds();
539                double splitChildWeight = splitChild.getWeight();
540
541                if (!splitChildren.hasNext()) {
542                    double newHeight = bounds.getMaxY() - y;
543                    Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
544                    layout2(splitChild, newSplitChildBounds);
545                }
546                else if ((availableHeight > 0.0) && (splitChildWeight > 0.0)) {
547                    double allocatedHeight = (splitChild.equals(lastWeightedChild))
548                    ? availableHeight
549                            : Math.rint(splitChildWeight * extraHeight);
550                    double newHeight = splitChildBounds.getHeight() + allocatedHeight;
551                    Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
552                    layout2(splitChild, newSplitChildBounds);
553                    availableHeight -= allocatedHeight;
554                }
555                else {
556                    double existingHeight = splitChildBounds.getHeight();
557                    Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, existingHeight);
558                    layout2(splitChild, newSplitChildBounds);
559                }
560                y = splitChild.getBounds().getMaxY();
561            }
562        }
563    }
564
565    /* Second pass of the layout algorithm: branch to layoutGrow/Shrink
566     * as needed.
567     */
568    private void layout2(Node root, Rectangle bounds) {
569        if (root instanceof Leaf) {
570            Component child = childForNode(root);
571            if (child != null) {
572                child.setBounds(bounds);
573            }
574            root.setBounds(bounds);
575        }
576        else if (root instanceof Divider) {
577            root.setBounds(bounds);
578        }
579        else if (root instanceof Split) {
580            Split split = (Split)root;
581            boolean grow = split.isRowLayout()
582            ? (split.getBounds().width <= bounds.width)
583                    : (split.getBounds().height <= bounds.height);
584            if (grow) {
585                layoutGrow(split, bounds);
586                root.setBounds(bounds);
587            }
588            else {
589                layoutShrink(split, bounds);
590                // split.setBounds() called in layoutShrink()
591            }
592        }
593    }
594
595    /* First pass of the layout algorithm.
596     *
597     * If the Dividers are "floating" then set the bounds of each
598     * node to accomodate the preferred size of all of the
599     * Leaf's java.awt.Components.  Otherwise, just set the bounds
600     * of each Leaf/Split node so that it's to the left of (for
601     * Split.isRowLayout() Split children) or directly above
602     * the Divider that follows.
603     *
604     * This pass sets the bounds of each Node in the layout model.  It
605     * does not resize any of the parent Container's
606     * (java.awt.Component) children.  That's done in the second pass,
607     * see layoutGrow() and layoutShrink().
608     */
609    private void layout1(Node root, Rectangle bounds) {
610        if (root instanceof Leaf) {
611            root.setBounds(bounds);
612        }
613        else if (root instanceof Split) {
614            Split split = (Split)root;
615            Iterator<Node> splitChildren = split.getChildren().iterator();
616            Rectangle childBounds = null;
617            int dividerSize = getDividerSize();
618
619            /* Layout the Split's child Nodes' along the X axis.  The bounds
620             * of each child will have the same y coordinate and height as the
621             * layout1() bounds argument.
622             *
623             * Note: the column layout code - that's the "else" clause below
624             * this if, is identical to the X axis (rowLayout) code below.
625             */
626            if (split.isRowLayout()) {
627                double x = bounds.getX();
628                while(splitChildren.hasNext()) {
629                    Node splitChild = splitChildren.next();
630                    Divider dividerChild =
631                        (splitChildren.hasNext()) ? (Divider)(splitChildren.next()) : null;
632
633                        double childWidth = 0.0;
634                        if (getFloatingDividers()) {
635                            childWidth = preferredNodeSize(splitChild).getWidth();
636                        }
637                        else {
638                            if (dividerChild != null) {
639                                childWidth = dividerChild.getBounds().getX() - x;
640                            }
641                            else {
642                                childWidth = split.getBounds().getMaxX() - x;
643                            }
644                        }
645                        childBounds = boundsWithXandWidth(bounds, x, childWidth);
646                        layout1(splitChild, childBounds);
647
648                        if (getFloatingDividers() && (dividerChild != null)) {
649                            double dividerX = childBounds.getMaxX();
650                            Rectangle dividerBounds = boundsWithXandWidth(bounds, dividerX, dividerSize);
651                            dividerChild.setBounds(dividerBounds);
652                        }
653                        if (dividerChild != null) {
654                            x = dividerChild.getBounds().getMaxX();
655                        }
656                }
657            }
658
659            /* Layout the Split's child Nodes' along the Y axis.  The bounds
660             * of each child will have the same x coordinate and width as the
661             * layout1() bounds argument.  The algorithm is identical to what's
662             * explained above, for the X axis case.
663             */
664            else {
665                double y = bounds.getY();
666                while(splitChildren.hasNext()) {
667                    Node splitChild = splitChildren.next();
668                    Divider dividerChild =
669                        (splitChildren.hasNext()) ? (Divider)(splitChildren.next()) : null;
670
671                        double childHeight = 0.0;
672                        if (getFloatingDividers()) {
673                            childHeight = preferredNodeSize(splitChild).getHeight();
674                        }
675                        else {
676                            if (dividerChild != null) {
677                                childHeight = dividerChild.getBounds().getY() - y;
678                            }
679                            else {
680                                childHeight = split.getBounds().getMaxY() - y;
681                            }
682                        }
683                        childBounds = boundsWithYandHeight(bounds, y, childHeight);
684                        layout1(splitChild, childBounds);
685
686                        if (getFloatingDividers() && (dividerChild != null)) {
687                            double dividerY = childBounds.getMaxY();
688                            Rectangle dividerBounds = boundsWithYandHeight(bounds, dividerY, dividerSize);
689                            dividerChild.setBounds(dividerBounds);
690                        }
691                        if (dividerChild != null) {
692                            y = dividerChild.getBounds().getMaxY();
693                        }
694                }
695            }
696            /* The bounds of the Split node root are set to be just
697             * big enough to contain all of its children, but only
698             * along the axis it's allocating space on.  That's
699             * X for rows, Y for columns.  The second pass of the
700             * layout algorithm - see layoutShrink()/layoutGrow()
701             * allocates extra space.
702             */
703            minimizeSplitBounds(split, bounds);
704        }
705    }
706
707    /**
708     * The specified Node is either the wrong type or was configured
709     * incorrectly.
710     */
711    public static class InvalidLayoutException extends RuntimeException {
712        private final Node node;
713        public InvalidLayoutException (String msg, Node node) {
714            super(msg);
715            this.node = node;
716        }
717        /**
718         * @return the invalid Node.
719         */
720        public Node getNode() { return node; }
721    }
722
723    private void throwInvalidLayout(String msg, Node node) {
724        throw new InvalidLayoutException(msg, node);
725    }
726
727    private void checkLayout(Node root) {
728        if (root instanceof Split) {
729            Split split = (Split)root;
730            if (split.getChildren().size() <= 2) {
731                throwInvalidLayout("Split must have > 2 children", root);
732            }
733            Iterator<Node> splitChildren = split.getChildren().iterator();
734            double weight = 0.0;
735            while(splitChildren.hasNext()) {
736                Node splitChild = splitChildren.next();
737                if (splitChild instanceof Divider) {
738                    throwInvalidLayout("expected a Split or Leaf Node", splitChild);
739                }
740                if (splitChildren.hasNext()) {
741                    Node dividerChild = splitChildren.next();
742                    if (!(dividerChild instanceof Divider)) {
743                        throwInvalidLayout("expected a Divider Node", dividerChild);
744                    }
745                }
746                weight += splitChild.getWeight();
747                checkLayout(splitChild);
748            }
749            if (weight > 1.0 + 0.000000001) { /* add some epsilon to a double check */
750                throwInvalidLayout("Split children's total weight > 1.0", root);
751            }
752        }
753    }
754
755    /**
756     * Compute the bounds of all of the Split/Divider/Leaf Nodes in
757     * the layout model, and then set the bounds of each child component
758     * with a matching Leaf Node.
759     */
760    public void layoutContainer(Container parent) {
761        checkLayout(getModel());
762        Insets insets = parent.getInsets();
763        Dimension size = parent.getSize();
764        int width = size.width - (insets.left + insets.right);
765        int height = size.height - (insets.top + insets.bottom);
766        Rectangle bounds = new Rectangle(insets.left, insets.top, width, height);
767        layout1(getModel(), bounds);
768        layout2(getModel(), bounds);
769    }
770
771    private Divider dividerAt(Node root, int x, int y) {
772        if (root instanceof Divider) {
773            Divider divider = (Divider)root;
774            return (divider.getBounds().contains(x, y)) ? divider : null;
775        }
776        else if (root instanceof Split) {
777            Split split = (Split)root;
778            for(Node child : split.getChildren()) {
779                if (child.getBounds().contains(x, y))
780                    return dividerAt(child, x, y);
781            }
782        }
783        return null;
784    }
785
786    /**
787     * Return the Divider whose bounds contain the specified
788     * point, or null if there isn't one.
789     *
790     * @param x x coordinate
791     * @param y y coordinate
792     * @return the Divider at x,y
793     */
794    public Divider dividerAt(int x, int y) {
795        return dividerAt(getModel(), x, y);
796    }
797
798    private boolean nodeOverlapsRectangle(Node node, Rectangle r2) {
799        Rectangle r1 = node.getBounds();
800        return
801        (r1.x <= (r2.x + r2.width)) && ((r1.x + r1.width) >= r2.x) &&
802        (r1.y <= (r2.y + r2.height)) && ((r1.y + r1.height) >= r2.y);
803    }
804
805    private List<Divider> dividersThatOverlap(Node root, Rectangle r) {
806        if (nodeOverlapsRectangle(root, r) && (root instanceof Split)) {
807            List<Divider> dividers = new ArrayList<Divider>();
808            for(Node child : ((Split)root).getChildren()) {
809                if (child instanceof Divider) {
810                    if (nodeOverlapsRectangle(child, r)) {
811                        dividers.add((Divider)child);
812                    }
813                }
814                else if (child instanceof Split) {
815                    dividers.addAll(dividersThatOverlap(child, r));
816                }
817            }
818            return dividers;
819        } else
820            return Collections.emptyList();
821    }
822
823    /**
824     * Return the Dividers whose bounds overlap the specified
825     * Rectangle.
826     *
827     * @param r target Rectangle
828     * @return the Dividers that overlap r
829     * @throws IllegalArgumentException if the Rectangle is null
830     */
831    public List<Divider> dividersThatOverlap(Rectangle r) {
832        if (r == null)
833            throw new IllegalArgumentException("null Rectangle");
834        return dividersThatOverlap(getModel(), r);
835    }
836
837    /**
838     * Base class for the nodes that model a MultiSplitLayout.
839     */
840    public static abstract class Node {
841        private Split parent = null;
842        private Rectangle bounds = new Rectangle();
843        private double weight = 0.0;
844
845        /**
846         * Returns the Split parent of this Node, or null.
847         *
848         * @return the value of the parent property.
849         * @see #setParent
850         */
851        public Split getParent() { return parent; }
852
853        /**
854         * Set the value of this Node's parent property.  The default
855         * value of this property is null.
856         *
857         * @param parent a Split or null
858         * @see #getParent
859         */
860        public void setParent(Split parent) {
861            this.parent = parent;
862        }
863
864        /**
865         * Returns the bounding Rectangle for this Node.
866         *
867         * @return the value of the bounds property.
868         * @see #setBounds
869         */
870        public Rectangle getBounds() {
871            return new Rectangle(this.bounds);
872        }
873
874        /**
875         * Set the bounding Rectangle for this node.  The value of
876         * bounds may not be null.  The default value of bounds
877         * is equal to <code>new Rectangle(0,0,0,0)</code>.
878         *
879         * @param bounds the new value of the bounds property
880         * @throws IllegalArgumentException if bounds is null
881         * @see #getBounds
882         */
883        public void setBounds(Rectangle bounds) {
884            if (bounds == null)
885                throw new IllegalArgumentException("null bounds");
886            this.bounds = new Rectangle(bounds);
887        }
888
889        /**
890         * Value between 0.0 and 1.0 used to compute how much space
891         * to add to this sibling when the layout grows or how
892         * much to reduce when the layout shrinks.
893         *
894         * @return the value of the weight property
895         * @see #setWeight
896         */
897        public double getWeight() { return weight; }
898
899        /**
900         * The weight property is a between 0.0 and 1.0 used to
901         * compute how much space to add to this sibling when the
902         * layout grows or how much to reduce when the layout shrinks.
903         * If rowLayout is true then this node's width grows
904         * or shrinks by (extraSpace * weight).  If rowLayout is false,
905         * then the node's height is changed.  The default value
906         * of weight is 0.0.
907         *
908         * @param weight a double between 0.0 and 1.0
909         * @see #getWeight
910         * @see MultiSplitLayout#layoutContainer
911         * @throws IllegalArgumentException if weight is not between 0.0 and 1.0
912         */
913        public void setWeight(double weight) {
914            if ((weight < 0.0)|| (weight > 1.0))
915                throw new IllegalArgumentException("invalid weight");
916            this.weight = weight;
917        }
918
919        private Node siblingAtOffset(int offset) {
920            Split parent = getParent();
921            if (parent == null)
922                return null;
923            List<Node> siblings = parent.getChildren();
924            int index = siblings.indexOf(this);
925            if (index == -1)
926                return null;
927            index += offset;
928            return ((index > -1) && (index < siblings.size())) ? siblings.get(index) : null;
929        }
930
931        /**
932         * Return the Node that comes after this one in the parent's
933         * list of children, or null.  If this node's parent is null,
934         * or if it's the last child, then return null.
935         *
936         * @return the Node that comes after this one in the parent's list of children.
937         * @see #previousSibling
938         * @see #getParent
939         */
940        public Node nextSibling() {
941            return siblingAtOffset(+1);
942        }
943
944        /**
945         * Return the Node that comes before this one in the parent's
946         * list of children, or null.  If this node's parent is null,
947         * or if it's the last child, then return null.
948         *
949         * @return the Node that comes before this one in the parent's list of children.
950         * @see #nextSibling
951         * @see #getParent
952         */
953        public Node previousSibling() {
954            return siblingAtOffset(-1);
955        }
956    }
957
958    /**
959     * Defines a vertical or horizontal subdivision into two or more
960     * tiles.
961     */
962    public static class Split extends Node {
963        private List<Node> children = Collections.emptyList();
964        private boolean rowLayout = true;
965
966        /**
967         * Returns true if the this Split's children are to be
968         * laid out in a row: all the same height, left edge
969         * equal to the previous Node's right edge.  If false,
970         * children are laid on in a column.
971         *
972         * @return the value of the rowLayout property.
973         * @see #setRowLayout
974         */
975        public boolean isRowLayout() { return rowLayout; }
976
977        /**
978         * Set the rowLayout property.  If true, all of this Split's
979         * children are to be laid out in a row: all the same height,
980         * each node's left edge equal to the previous Node's right
981         * edge.  If false, children are laid on in a column.  Default
982         * value is true.
983         *
984         * @param rowLayout true for horizontal row layout, false for column
985         * @see #isRowLayout
986         */
987        public void setRowLayout(boolean rowLayout) {
988            this.rowLayout = rowLayout;
989        }
990
991        /**
992         * Returns this Split node's children.  The returned value
993         * is not a reference to the Split's internal list of children
994         *
995         * @return the value of the children property.
996         * @see #setChildren
997         */
998        public List<Node> getChildren() {
999            return new ArrayList<Node>(children);
1000        }
1001
1002        /**
1003         * Set's the children property of this Split node.  The parent
1004         * of each new child is set to this Split node, and the parent
1005         * of each old child (if any) is set to null.  This method
1006         * defensively copies the incoming List.  Default value is
1007         * an empty List.
1008         *
1009         * @param children List of children
1010         * @see #getChildren
1011         * @throws IllegalArgumentException if children is null
1012         */
1013        public void setChildren(List<Node> children) {
1014            if (children == null)
1015                throw new IllegalArgumentException("children must be a non-null List");
1016            for(Node child : this.children) {
1017                child.setParent(null);
1018            }
1019            this.children = new ArrayList<Node>(children);
1020            for(Node child : this.children) {
1021                child.setParent(this);
1022            }
1023        }
1024
1025        /**
1026         * Convenience method that returns the last child whose weight
1027         * is > 0.0.
1028         *
1029         * @return the last child whose weight is > 0.0.
1030         * @see #getChildren
1031         * @see Node#getWeight
1032         */
1033        public final Node lastWeightedChild() {
1034            List<Node> children = getChildren();
1035            Node weightedChild = null;
1036            for(Node child : children) {
1037                if (child.getWeight() > 0.0) {
1038                    weightedChild = child;
1039                }
1040            }
1041            return weightedChild;
1042        }
1043
1044        @Override
1045        public String toString() {
1046            int nChildren = getChildren().size();
1047            StringBuffer sb = new StringBuffer("MultiSplitLayout.Split");
1048            sb.append(isRowLayout() ? " ROW [" : " COLUMN [");
1049            sb.append(nChildren + ((nChildren == 1) ? " child" : " children"));
1050            sb.append("] ");
1051            sb.append(getBounds());
1052            return sb.toString();
1053        }
1054    }
1055
1056    /**
1057     * Models a java.awt Component child.
1058     */
1059    public static class Leaf extends Node {
1060        private String name = "";
1061
1062        /**
1063         * Create a Leaf node.  The default value of name is "".
1064         */
1065        public Leaf() { }
1066
1067        /**
1068         * Create a Leaf node with the specified name.  Name can not
1069         * be null.
1070         *
1071         * @param name value of the Leaf's name property
1072         * @throws IllegalArgumentException if name is null
1073         */
1074        public Leaf(String name) {
1075            if (name == null)
1076                throw new IllegalArgumentException("name is null");
1077            this.name = name;
1078        }
1079
1080        /**
1081         * Return the Leaf's name.
1082         *
1083         * @return the value of the name property.
1084         * @see #setName
1085         */
1086        public String getName() { return name; }
1087
1088        /**
1089         * Set the value of the name property.  Name may not be null.
1090         *
1091         * @param name value of the name property
1092         * @throws IllegalArgumentException if name is null
1093         */
1094        public void setName(String name) {
1095            if (name == null)
1096                throw new IllegalArgumentException("name is null");
1097            this.name = name;
1098        }
1099
1100        @Override
1101        public String toString() {
1102            StringBuffer sb = new StringBuffer("MultiSplitLayout.Leaf");
1103            sb.append(" \"");
1104            sb.append(getName());
1105            sb.append("\"");
1106            sb.append(" weight=");
1107            sb.append(getWeight());
1108            sb.append(" ");
1109            sb.append(getBounds());
1110            return sb.toString();
1111        }
1112    }
1113
1114    /**
1115     * Models a single vertical/horiztonal divider.
1116     */
1117    public static class Divider extends Node {
1118        /**
1119         * Convenience method, returns true if the Divider's parent
1120         * is a Split row (a Split with isRowLayout() true), false
1121         * otherwise. In other words if this Divider's major axis
1122         * is vertical, return true.
1123         *
1124         * @return true if this Divider is part of a Split row.
1125         */
1126        public final boolean isVertical() {
1127            Split parent = getParent();
1128            return (parent != null) ? parent.isRowLayout() : false;
1129        }
1130
1131        /**
1132         * Dividers can't have a weight, they don't grow or shrink.
1133         * @throws UnsupportedOperationException
1134         */
1135        @Override
1136        public void setWeight(double weight) {
1137            throw new UnsupportedOperationException();
1138        }
1139
1140        @Override
1141        public String toString() {
1142            return "MultiSplitLayout.Divider " + getBounds().toString();
1143        }
1144    }
1145
1146    private static void throwParseException(StreamTokenizer st, String msg) throws Exception {
1147        throw new Exception("MultiSplitLayout.parseModel Error: " + msg);
1148    }
1149
1150    private static void parseAttribute(String name, StreamTokenizer st, Node node) throws Exception {
1151        if ((st.nextToken() != '=')) {
1152            throwParseException(st, "expected '=' after " + name);
1153        }
1154        if (name.equalsIgnoreCase("WEIGHT")) {
1155            if (st.nextToken() == StreamTokenizer.TT_NUMBER) {
1156                node.setWeight(st.nval);
1157            }
1158            else {
1159                throwParseException(st, "invalid weight");
1160            }
1161        }
1162        else if (name.equalsIgnoreCase("NAME")) {
1163            if (st.nextToken() == StreamTokenizer.TT_WORD) {
1164                if (node instanceof Leaf) {
1165                    ((Leaf)node).setName(st.sval);
1166                }
1167                else {
1168                    throwParseException(st, "can't specify name for " + node);
1169                }
1170            }
1171            else {
1172                throwParseException(st, "invalid name");
1173            }
1174        }
1175        else {
1176            throwParseException(st, "unrecognized attribute \"" + name + "\"");
1177        }
1178    }
1179
1180    private static void addSplitChild(Split parent, Node child) {
1181        List<Node> children = new ArrayList<Node>(parent.getChildren());
1182        if (children.size() == 0) {
1183            children.add(child);
1184        }
1185        else {
1186            children.add(new Divider());
1187            children.add(child);
1188        }
1189        parent.setChildren(children);
1190    }
1191
1192    private static void parseLeaf(StreamTokenizer st, Split parent) throws Exception {
1193        Leaf leaf = new Leaf();
1194        int token;
1195        while ((token = st.nextToken()) != StreamTokenizer.TT_EOF) {
1196            if (token == ')') {
1197                break;
1198            }
1199            if (token == StreamTokenizer.TT_WORD) {
1200                parseAttribute(st.sval, st, leaf);
1201            }
1202            else {
1203                throwParseException(st, "Bad Leaf: " + leaf);
1204            }
1205        }
1206        addSplitChild(parent, leaf);
1207    }
1208
1209    private static void parseSplit(StreamTokenizer st, Split parent) throws Exception {
1210        int token;
1211        while ((token = st.nextToken()) != StreamTokenizer.TT_EOF) {
1212            if (token == ')') {
1213                break;
1214            }
1215            else if (token == StreamTokenizer.TT_WORD) {
1216                if (st.sval.equalsIgnoreCase("WEIGHT")) {
1217                    parseAttribute(st.sval, st, parent);
1218                }
1219                else {
1220                    addSplitChild(parent, new Leaf(st.sval));
1221                }
1222            }
1223            else if (token == '(') {
1224                if ((token = st.nextToken()) != StreamTokenizer.TT_WORD) {
1225                    throwParseException(st, "invalid node type");
1226                }
1227                String nodeType = st.sval.toUpperCase();
1228                if (nodeType.equals("LEAF")) {
1229                    parseLeaf(st, parent);
1230                }
1231                else if (nodeType.equals("ROW") || nodeType.equals("COLUMN")) {
1232                    Split split = new Split();
1233                    split.setRowLayout(nodeType.equals("ROW"));
1234                    addSplitChild(parent, split);
1235                    parseSplit(st, split);
1236                }
1237                else {
1238                    throwParseException(st, "unrecognized node type '" + nodeType + "'");
1239                }
1240            }
1241        }
1242    }
1243
1244    private static Node parseModel (Reader r) {
1245        StreamTokenizer st = new StreamTokenizer(r);
1246        try {
1247            Split root = new Split();
1248            parseSplit(st, root);
1249            return root.getChildren().get(0);
1250        }
1251        catch (Exception e) {
1252            System.err.println(e);
1253        }
1254        finally {
1255            try { r.close(); } catch (IOException ignore) {}
1256        }
1257        return null;
1258    }
1259
1260    /**
1261     * A convenience method that converts a string to a
1262     * MultiSplitLayout model (a tree of Nodes) using a
1263     * a simple syntax.  Nodes are represented by
1264     * parenthetical expressions whose first token
1265     * is one of ROW/COLUMN/LEAF.  ROW and COLUMN specify
1266     * horizontal and vertical Split nodes respectively,
1267     * LEAF specifies a Leaf node.  A Leaf's name and
1268     * weight can be specified with attributes,
1269     * name=<i>myLeafName</i> weight=<i>myLeafWeight</i>.
1270     * Similarly, a Split's weight can be specified with
1271     * weight=<i>mySplitWeight</i>.
1272     *
1273     * <p> For example, the following expression generates
1274     * a horizontal Split node with three children:
1275     * the Leafs named left and right, and a Divider in
1276     * between:
1277     * <pre>
1278     * (ROW (LEAF name=left) (LEAF name=right weight=1.0))
1279     * </pre>
1280     *
1281     * <p> Dividers should not be included in the string,
1282     * they're added automatcially as needed.  Because
1283     * Leaf nodes often only need to specify a name, one
1284     * can specify a Leaf by just providing the name.
1285     * The previous example can be written like this:
1286     * <pre>
1287     * (ROW left (LEAF name=right weight=1.0))
1288     * </pre>
1289     *
1290     * <p>Here's a more complex example.  One row with
1291     * three elements, the first and last of which are columns
1292     * with two leaves each:
1293     * <pre>
1294     * (ROW (COLUMN weight=0.5 left.top left.bottom)
1295     *      (LEAF name=middle)
1296     *      (COLUMN weight=0.5 right.top right.bottom))
1297     * </pre>
1298     *
1299     *
1300     * <p> This syntax is not intended for archiving or
1301     * configuration files .  It's just a convenience for
1302     * examples and tests.
1303     *
1304     * @return the Node root of a tree based on s.
1305     */
1306    public static Node parseModel(String s) {
1307        return parseModel(new StringReader(s));
1308    }
1309
1310    private static void printModel(String indent, Node root) {
1311        if (root instanceof Split) {
1312            Split split = (Split)root;
1313            System.out.println(indent + split);
1314            for(Node child : split.getChildren()) {
1315                printModel(indent + "  ", child);
1316            }
1317        }
1318        else {
1319            System.out.println(indent + root);
1320        }
1321    }
1322
1323    /**
1324     * Print the tree with enough detail for simple debugging.
1325     */
1326    public static void printModel(Node root) {
1327        printModel("", root);
1328    }
1329}
Note: See TracBrowser for help on using the repository browser.