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

Last change on this file since 13652 was 13265, checked in by Don-vip, 6 years ago

see #15709 - fix a lot of memory leaks. Now gui.layer.geoImage.* classes are correctly garbage collected when the mapframe is destroyed

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