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

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

fix some Sonar issues

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