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