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

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

refactor of some GUI/widgets classes (impacts some plugins):

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