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

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

see #19334 - javadoc fixes + protected constructors for abstract classes

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