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

Last change on this file since 3779 was 3095, checked in by jttt, 14 years ago

Changes in multipolygon handling (see #4661), cosmetics

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