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

Last change on this file since 3083 was 3083, checked in by bastiK, 14 years ago

added svn:eol-style=native to source files

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