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

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

checkstyle: enable relevant whitespace checks and fix them

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