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

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

code style - A close curly brace should be located at the beginning of a line

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