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