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