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

Last change on this file since 2227 was 2227, checked in by stoecker, 15 years ago

fixed #3640 - double checks against fixed numbers are dangerous

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