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

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

When doing a String.toLowerCase()/toUpperCase() call, use a Locale. This avoids problems with certain locales, i.e. Lithuanian or Turkish. See PMD UseLocaleWithCaseConversions rule and String.toLowerCase() javadoc.

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