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

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

code style - Useless parentheses around expressions should be removed to prevent any misunderstanding

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