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

Last change on this file since 6296 was 6296, checked in by Don-vip, 5 years ago

Sonar/Findbugs - Avoid commented-out lines of code, javadoc

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