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

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

sonar - fix consecutive literal appends

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