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

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

SpotBugs - fix some warnings

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