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

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

global cleanup of IllegalArgumentExceptions thrown by JOSM

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