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

Last change on this file since 6070 was 5903, checked in by stoecker, 11 years ago

fix javadoc

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