001/* 002 * $Id: MultiSplitLayout.java 3957 2011-03-15 18:27:26Z kschaefe $ 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 022package org.jdesktop.swingx; 023 024import java.awt.Component; 025import java.awt.Container; 026import java.awt.Dimension; 027import java.awt.Insets; 028import java.awt.LayoutManager; 029import java.awt.Rectangle; 030import java.beans.PropertyChangeListener; 031import java.beans.PropertyChangeSupport; 032import java.io.IOException; 033import java.io.Reader; 034import java.io.Serializable; 035import java.io.StreamTokenizer; 036import java.io.StringReader; 037import java.util.ArrayList; 038import java.util.Arrays; 039import java.util.Collections; 040import java.util.HashMap; 041import java.util.Iterator; 042import java.util.List; 043import java.util.ListIterator; 044import java.util.Map; 045 046import javax.swing.UIManager; 047 048 049/** 050 * The MultiSplitLayout layout manager recursively arranges its 051 * components in row and column groups called "Splits". Elements of 052 * the layout are separated by gaps called "Dividers". The overall 053 * layout is defined with a simple tree model whose nodes are 054 * instances of MultiSplitLayout.Split, MultiSplitLayout.Divider, 055 * and MultiSplitLayout.Leaf. Named Leaf nodes represent the space 056 * allocated to a component that was added with a constraint that 057 * matches the Leaf's name. Extra space is distributed 058 * among row/column siblings according to their 0.0 to 1.0 weight. 059 * If no weights are specified then the last sibling always gets 060 * all of the extra space, or space reduction. 061 * 062 * <p> 063 * Although MultiSplitLayout can be used with any Container, it's 064 * the default layout manager for MultiSplitPane. MultiSplitPane 065 * supports interactively dragging the Dividers, accessibility, 066 * and other features associated with split panes. 067 * 068 * <p> 069 * All properties in this class are bound: when a properties value 070 * is changed, all PropertyChangeListeners are fired. 071 * 072 * 073 * @author Hans Muller 074 * @author Luan O'Carroll 075 * @see JXMultiSplitPane 076 */ 077 078/* 079 * Changes by Luan O'Carroll 080 * 1 Support for visibility added. 081 */ 082public class MultiSplitLayout implements LayoutManager, Serializable 083{ 084 public static final int DEFAULT_LAYOUT = 0; 085 public static final int NO_MIN_SIZE_LAYOUT = 1; 086 public static final int USER_MIN_SIZE_LAYOUT = 2; 087 088 private final Map<String, Component> childMap = new HashMap<String, Component>(); 089 private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); 090 private Node model; 091 private int dividerSize; 092 private boolean floatingDividers = true; 093 094 private boolean removeDividers = true; 095 private boolean layoutByWeight = false; 096 097 private int layoutMode; 098 private int userMinSize = 20; 099 100 /** 101 * Create a MultiSplitLayout with a default model with a single 102 * Leaf node named "default". 103 * 104 * #see setModel 105 */ 106 public MultiSplitLayout() 107 { 108 this(new Leaf("default")); 109 } 110 111 /** 112 * Create a MultiSplitLayout with a default model with a single 113 * Leaf node named "default". 114 * 115 * @param layoutByWeight if true the layout is initialized in proportion to 116 * the node weights rather than the component preferred sizes. 117 * #see setModel 118 */ 119 public MultiSplitLayout(boolean layoutByWeight) 120 { 121 this(new Leaf("default")); 122 this.layoutByWeight = layoutByWeight; 123 } 124 125 /** 126 * Set the size of the child components to match the weights of the children. 127 * If the components to not all specify a weight then the available layout 128 * space is divided equally between the components. 129 */ 130 public void layoutByWeight( Container parent ) 131 { 132 doLayoutByWeight( parent ); 133 134 layoutContainer( parent ); 135 } 136 137 /** 138 * Set the size of the child components to match the weights of the children. 139 * If the components to not all specify a weight then the available layout 140 * space is divided equally between the components. 141 */ 142 private void doLayoutByWeight( Container parent ) 143 { 144 Dimension size = parent.getSize(); 145 Insets insets = parent.getInsets(); 146 int width = size.width - (insets.left + insets.right); 147 int height = size.height - (insets.top + insets.bottom); 148 Rectangle bounds = new Rectangle(insets.left, insets.top, width, height); 149 150 if (model instanceof Leaf) 151 model.setBounds(bounds); 152 else if (model instanceof Split) 153 doLayoutByWeight( model, bounds ); 154 } 155 156 private void doLayoutByWeight( Node node, Rectangle bounds ) 157 { 158 int width = bounds.width; 159 int height = bounds.height; 160 Split split = (Split)node; 161 List<Node> splitChildren = split.getChildren(); 162 double distributableWeight = 1.0; 163 int unweightedComponents = 0; 164 int dividerSpace = 0; 165 for( Node splitChild : splitChildren ) { 166 if ( !splitChild.isVisible()) 167 continue; 168 else if ( splitChild instanceof Divider ) { 169 dividerSpace += dividerSize; 170 continue; 171 } 172 173 double weight = splitChild.getWeight(); 174 if ( weight > 0.0 ) 175 distributableWeight -= weight; 176 else 177 unweightedComponents++; 178 } 179 180 if ( split.isRowLayout()) { 181 width -= dividerSpace; 182 double distributableWidth = width * distributableWeight; 183 for( Node splitChild : splitChildren ) { 184 if ( !splitChild.isVisible() || ( splitChild instanceof Divider )) 185 continue; 186 187 double weight = splitChild.getWeight(); 188 Rectangle splitChildBounds = splitChild.getBounds(); 189 if ( weight >= 0 ) 190 splitChildBounds = new Rectangle( splitChildBounds.x, splitChildBounds.y, (int)( width * weight ), height ); 191 else 192 splitChildBounds = new Rectangle( splitChildBounds.x, splitChildBounds.y, (int)( distributableWidth / unweightedComponents ), height ); 193 194 if ( layoutMode == USER_MIN_SIZE_LAYOUT ) { 195 splitChildBounds.setSize( Math.max( splitChildBounds.width, userMinSize ), splitChildBounds.height ); 196 } 197 198 splitChild.setBounds( splitChildBounds ); 199 200 if ( splitChild instanceof Split ) 201 doLayoutByWeight( splitChild, splitChildBounds ); 202 else { 203 Component comp = getComponentForNode( splitChild ); 204 if ( comp != null ) 205 comp.setPreferredSize( splitChildBounds.getSize()); 206 } 207 } 208 } 209 else { 210 height -= dividerSpace; 211 double distributableHeight = height * distributableWeight; 212 for( Node splitChild : splitChildren ) { 213 if ( !splitChild.isVisible() || ( splitChild instanceof Divider )) 214 continue; 215 216 double weight = splitChild.getWeight(); 217 Rectangle splitChildBounds = splitChild.getBounds(); 218 if ( weight >= 0 ) 219 splitChildBounds = new Rectangle( splitChildBounds.x, splitChildBounds.y, width, (int)( height * weight )); 220 else 221 splitChildBounds = new Rectangle( splitChildBounds.x, splitChildBounds.y, width, (int)( distributableHeight / unweightedComponents )); 222 223 if ( layoutMode == USER_MIN_SIZE_LAYOUT ) { 224 splitChildBounds.setSize( splitChildBounds.width, Math.max( splitChildBounds.height, userMinSize ) ); 225 } 226 227 splitChild.setBounds( splitChildBounds ); 228 229 if ( splitChild instanceof Split ) 230 doLayoutByWeight( splitChild, splitChildBounds ); 231 else { 232 Component comp = getComponentForNode( splitChild ); 233 if ( comp != null ) 234 comp.setPreferredSize( splitChildBounds.getSize()); 235 } 236 } 237 } 238 } 239 240 /** 241 * Get the component associated with a MultiSplitLayout.Node 242 * @param n the layout node 243 * @return the component handled by the layout or null if not found 244 */ 245 public Component getComponentForNode( Node n ) 246 { 247 String name = ((Leaf)n).getName(); 248 return (name != null) ? (Component)childMap.get(name) : null; 249 } 250 251 /** 252 * Get the MultiSplitLayout.Node associated with a component 253 * @param comp the component being positioned by the layout 254 * @return the node associated with the component 255 */ 256 public Node getNodeForComponent( Component comp ) 257 { 258 return getNodeForName( getNameForComponent( comp )); 259 } 260 261 /** 262 * Get the MultiSplitLayout.Node associated with a component 263 * @param name the name used to associate a component with the layout 264 * @return the node associated with the component 265 */ 266 public Node getNodeForName( String name ) 267 { 268 if ( model instanceof Split ) { 269 Split split = ((Split)model); 270 return getNodeForName( split, name ); 271 } 272 else 273 return null; 274 } 275 276 /** 277 * Get the name used to map a component 278 * @param child the component 279 * @return the name used to map the component or null if no mapping is found 280 */ 281 public String getNameForComponent( Component child ) 282 { 283 String name = null; 284 for(Map.Entry<String,Component> kv : childMap.entrySet()) { 285 if (kv.getValue() == child) { 286 name = kv.getKey(); 287 break; 288 } 289 } 290 291 return name; 292 } 293 294 /** 295 * Get the MultiSplitLayout.Node associated with a component 296 * @param split the layout split that owns the requested node 297 * @param comp the component being positioned by the layout 298 * @return the node associated with the component 299 */ 300 public Node getNodeForComponent( Split split, Component comp ) 301 { 302 return getNodeForName( split, getNameForComponent( comp )); 303 } 304 305 /** 306 * Get the MultiSplitLayout.Node associated with a component 307 * @param split the layout split that owns the requested node 308 * @param name the name used to associate a component with the layout 309 * @return the node associated with the component 310 */ 311 public Node getNodeForName( Split split, String name ) 312 { 313 for(Node n : split.getChildren()) { 314 if ( n instanceof Leaf ) { 315 if ( ((Leaf)n).getName().equals( name )) 316 return n; 317 } 318 else if ( n instanceof Split ) { 319 Node n1 = getNodeForName( (Split)n, name ); 320 if ( n1 != null ) 321 return n1; 322 } 323 } 324 return null; 325 } 326 327 /** 328 * Is there a valid model for the layout? 329 * @return true if there is a model 330 */ 331 public boolean hasModel() 332 { 333 return model != null; 334 } 335 336 /** 337 * Create a MultiSplitLayout with the specified model. 338 * 339 * #see setModel 340 */ 341 public MultiSplitLayout(Node model) { 342 this.model = model; 343 this.dividerSize = UIManager.getInt("SplitPane.dividerSize"); 344 if (this.dividerSize == 0) { 345 this.dividerSize = 7; 346 } 347 } 348 349 public void addPropertyChangeListener(PropertyChangeListener listener) { 350 if (listener != null) { 351 pcs.addPropertyChangeListener(listener); 352 } 353 } 354 public void removePropertyChangeListener(PropertyChangeListener listener) { 355 if (listener != null) { 356 pcs.removePropertyChangeListener(listener); 357 } 358 } 359 public PropertyChangeListener[] getPropertyChangeListeners() { 360 return pcs.getPropertyChangeListeners(); 361 } 362 363 private void firePCS(String propertyName, Object oldValue, Object newValue) { 364 if (!(oldValue != null && newValue != null && oldValue.equals(newValue))) { 365 pcs.firePropertyChange(propertyName, oldValue, newValue); 366 } 367 } 368 369 /** 370 * Return the root of the tree of Split, Leaf, and Divider nodes 371 * that define this layout. 372 * 373 * @return the value of the model property 374 * @see #setModel 375 */ 376 public Node getModel() { return model; } 377 378 /** 379 * Set the root of the tree of Split, Leaf, and Divider nodes 380 * that define this layout. The model can be a Split node 381 * (the typical case) or a Leaf. The default value of this 382 * property is a Leaf named "default". 383 * 384 * @param model the root of the tree of Split, Leaf, and Divider node 385 * @throws IllegalArgumentException if model is a Divider or null 386 * @see #getModel 387 */ 388 public void setModel(Node model) { 389 if ((model == null) || (model instanceof Divider)) { 390 throw new IllegalArgumentException("invalid model"); 391 } 392 Node oldModel = getModel(); 393 this.model = model; 394 firePCS("model", oldModel, getModel()); 395 } 396 397 /** 398 * Returns the width of Dividers in Split rows, and the height of 399 * Dividers in Split columns. 400 * 401 * @return the value of the dividerSize property 402 * @see #setDividerSize 403 */ 404 public int getDividerSize() { return dividerSize; } 405 406 /** 407 * Sets the width of Dividers in Split rows, and the height of 408 * Dividers in Split columns. The default value of this property 409 * is the same as for JSplitPane Dividers. 410 * 411 * @param dividerSize the size of dividers (pixels) 412 * @throws IllegalArgumentException if dividerSize < 0 413 * @see #getDividerSize 414 */ 415 public void setDividerSize(int dividerSize) { 416 if (dividerSize < 0) { 417 throw new IllegalArgumentException("invalid dividerSize"); 418 } 419 int oldDividerSize = this.dividerSize; 420 this.dividerSize = dividerSize; 421 firePCS("dividerSize", new Integer( oldDividerSize ), new Integer( dividerSize )); 422 } 423 424 /** 425 * @return the value of the floatingDividers property 426 * @see #setFloatingDividers 427 */ 428 public boolean getFloatingDividers() { return floatingDividers; } 429 430 431 /** 432 * If true, Leaf node bounds match the corresponding component's 433 * preferred size and Splits/Dividers are resized accordingly. 434 * If false then the Dividers define the bounds of the adjacent 435 * Split and Leaf nodes. Typically this property is set to false 436 * after the (MultiSplitPane) user has dragged a Divider. 437 * 438 * @see #getFloatingDividers 439 */ 440 public void setFloatingDividers(boolean floatingDividers) 441 { 442 boolean oldFloatingDividers = this.floatingDividers; 443 this.floatingDividers = floatingDividers; 444 firePCS("floatingDividers", new Boolean( oldFloatingDividers ), new Boolean( floatingDividers )); 445 } 446 447 /** 448 * @return the value of the removeDividers property 449 * @see #setRemoveDividers 450 */ 451 public boolean getRemoveDividers() { return removeDividers; } 452 453 /** 454 * If true, the next divider is removed when a component is removed from the 455 * layout. If false, only the node itself is removed. Normally the next 456 * divider should be removed from the layout when a component is removed. 457 * @param removeDividers true to removed the next divider whena component is 458 * removed from teh layout 459 */ 460 public void setRemoveDividers( boolean removeDividers ) 461 { 462 boolean oldRemoveDividers = this.removeDividers; 463 this.removeDividers = removeDividers; 464 firePCS("removeDividers", new Boolean( oldRemoveDividers ), new Boolean( removeDividers )); 465 } 466 467 /** 468 * Add a component to this MultiSplitLayout. The 469 * <code>name</code> should match the name property of the Leaf 470 * node that represents the bounds of <code>child</code>. After 471 * layoutContainer() recomputes the bounds of all of the nodes in 472 * the model, it will set this child's bounds to the bounds of the 473 * Leaf node with <code>name</code>. Note: if a component was already 474 * added with the same name, this method does not remove it from 475 * its parent. 476 * 477 * @param name identifies the Leaf node that defines the child's bounds 478 * @param child the component to be added 479 * @see #removeLayoutComponent 480 */ 481 @Override 482public void addLayoutComponent(String name, Component child) { 483 if (name == null) { 484 throw new IllegalArgumentException("name not specified"); 485 } 486 childMap.put(name, child); 487 } 488 489 /** 490 * Removes the specified component from the layout. 491 * 492 * @param child the component to be removed 493 * @see #addLayoutComponent 494 */ 495 @Override 496public void removeLayoutComponent(Component child) { 497 String name = getNameForComponent( child ); 498 499 if ( name != null ) { 500 childMap.remove( name ); 501 } 502 } 503 504 /** 505 * Removes the specified node from the layout. 506 * 507 * @param name the name of the component to be removed 508 * @see #addLayoutComponent 509 */ 510 public void removeLayoutNode(String name) { 511 512 if ( name != null ) { 513 Node n; 514 if ( !( model instanceof Split )) 515 n = model; 516 else 517 n = getNodeForName( name ); 518 519 childMap.remove(name); 520 521 if ( n != null ) { 522 Split s = n.getParent(); 523 s.remove( n ); 524 if (removeDividers) { 525 while ( s.getChildren().size() < 2 ) { 526 Split p = s.getParent(); 527 if ( p == null ) { 528 if ( s.getChildren().size() > 0 ) 529 model = s.getChildren().get( 0 ); 530 else 531 model = null; 532 return; 533 } 534 if ( s.getChildren().size() == 1 ) { 535 Node next = s.getChildren().get( 0 ); 536 p.replace( s, next ); 537 next.setParent( p ); 538 } 539 else 540 p.remove( s ); 541 s = p; 542 } 543 } 544 } 545 else { 546 childMap.remove( name ); 547 } 548 } 549 } 550 551 /** 552 * Show/Hide nodes. Any dividers that are no longer required due to one of the 553 * nodes being made visible/invisible are also shown/hidden. The visibility of 554 * the component managed by the node is also changed by this method 555 * @param name the node name 556 * @param visible the new node visible state 557 */ 558 public void displayNode( String name, boolean visible ) 559 { 560 Node node = getNodeForName( name ); 561 if ( node != null ) { 562 Component comp = getComponentForNode( node ); 563 comp.setVisible( visible ); 564 node.setVisible( visible ); 565 566 MultiSplitLayout.Split p = node.getParent(); 567 if ( !visible ) { 568 p.hide( node ); 569 if ( !p.isVisible()) 570 p.getParent().hide( p ); 571 572 p.checkDividers( p ); 573 // If the split has become invisible then the parent may also have a 574 // divider that needs to be hidden. 575 while ( !p.isVisible()) { 576 p = p.getParent(); 577 if ( p != null ) 578 p.checkDividers( p ); 579 else 580 break; 581 } 582 } 583 else 584 p.restoreDividers( p ); 585 } 586 setFloatingDividers( false ); 587 } 588 589 private Component childForNode(Node node) { 590 if (node instanceof Leaf) { 591 Leaf leaf = (Leaf)node; 592 String name = leaf.getName(); 593 return (name != null) ? childMap.get(name) : null; 594 } 595 return null; 596 } 597 598 599 private Dimension preferredComponentSize(Node node) { 600 if ( layoutMode == NO_MIN_SIZE_LAYOUT ) 601 return new Dimension(0, 0); 602 603 Component child = childForNode(node); 604 return ((child != null) && child.isVisible() ) ? child.getPreferredSize() : new Dimension(0, 0); 605 } 606 607 private Dimension minimumComponentSize(Node node) { 608 if ( layoutMode == NO_MIN_SIZE_LAYOUT ) 609 return new Dimension(0, 0); 610 611 Component child = childForNode(node); 612 return ((child != null) && child.isVisible() ) ? child.getMinimumSize() : new Dimension(0, 0); 613 } 614 615 private Dimension preferredNodeSize(Node root) { 616 if (root instanceof Leaf) { 617 return preferredComponentSize(root); 618 } 619 else if (root instanceof Divider) { 620 if ( !((Divider)root).isVisible()) 621 return new Dimension(0,0); 622 int divSize = getDividerSize(); 623 return new Dimension(divSize, divSize); 624 } 625 else { 626 Split split = (Split)root; 627 List<Node> splitChildren = split.getChildren(); 628 int width = 0; 629 int height = 0; 630 if (split.isRowLayout()) { 631 for(Node splitChild : splitChildren) { 632 if ( !splitChild.isVisible()) 633 continue; 634 Dimension size = preferredNodeSize(splitChild); 635 width += size.width; 636 height = Math.max(height, size.height); 637 } 638 } 639 else { 640 for(Node splitChild : splitChildren) { 641 if ( !splitChild.isVisible()) 642 continue; 643 Dimension size = preferredNodeSize(splitChild); 644 width = Math.max(width, size.width); 645 height += size.height; 646 } 647 } 648 return new Dimension(width, height); 649 } 650 } 651 652 /** 653 * Get the minimum size of this node. Sums the minumum sizes of rows or 654 * columns to get the overall minimum size for the layout node, including the 655 * dividers. 656 * @param root the node whose size is required. 657 * @return the minimum size. 658 */ 659 public Dimension minimumNodeSize(Node root) { 660 assert( root.isVisible ); 661 if (root instanceof Leaf) { 662 if ( layoutMode == NO_MIN_SIZE_LAYOUT ) 663 return new Dimension(0, 0); 664 665 Component child = childForNode(root); 666 return ((child != null) && child.isVisible() ) ? child.getMinimumSize() : new Dimension(0, 0); 667 } 668 else if (root instanceof Divider) { 669 if ( !((Divider)root).isVisible() ) 670 return new Dimension(0,0); 671 int divSize = getDividerSize(); 672 return new Dimension(divSize, divSize); 673 } 674 else { 675 Split split = (Split)root; 676 List<Node> splitChildren = split.getChildren(); 677 int width = 0; 678 int height = 0; 679 if (split.isRowLayout()) { 680 for(Node splitChild : splitChildren) { 681 if ( !splitChild.isVisible()) 682 continue; 683 Dimension size = minimumNodeSize(splitChild); 684 width += size.width; 685 height = Math.max(height, size.height); 686 } 687 } 688 else { 689 for(Node splitChild : splitChildren) { 690 if ( !splitChild.isVisible()) 691 continue; 692 Dimension size = minimumNodeSize(splitChild); 693 width = Math.max(width, size.width); 694 height += size.height; 695 } 696 } 697 return new Dimension(width, height); 698 } 699 } 700 701 /** 702 * Get the maximum size of this node. Sums the minumum sizes of rows or 703 * columns to get the overall maximum size for the layout node, including the 704 * dividers. 705 * @param root the node whose size is required. 706 * @return the minimum size. 707 */ 708 public Dimension maximumNodeSize(Node root) { 709 assert( root.isVisible ); 710 if (root instanceof Leaf) { 711 Component child = childForNode(root); 712 return ((child != null) && child.isVisible() ) ? child.getMaximumSize() : new Dimension(0, 0); 713 } 714 else if (root instanceof Divider) { 715 if ( !((Divider)root).isVisible() ) 716 return new Dimension(0,0); 717 int divSize = getDividerSize(); 718 return new Dimension(divSize, divSize); 719 } 720 else { 721 Split split = (Split)root; 722 List<Node> splitChildren = split.getChildren(); 723 int width = Integer.MAX_VALUE; 724 int height = Integer.MAX_VALUE; 725 if (split.isRowLayout()) { 726 for(Node splitChild : splitChildren) { 727 if ( !splitChild.isVisible()) 728 continue; 729 Dimension size = maximumNodeSize(splitChild); 730 width += size.width; 731 height = Math.min(height, size.height); 732 } 733 } 734 else { 735 for(Node splitChild : splitChildren) { 736 if ( !splitChild.isVisible()) 737 continue; 738 Dimension size = maximumNodeSize(splitChild); 739 width = Math.min(width, size.width); 740 height += size.height; 741 } 742 } 743 return new Dimension(width, height); 744 } 745 } 746 747 private Dimension sizeWithInsets(Container parent, Dimension size) { 748 Insets insets = parent.getInsets(); 749 int width = size.width + insets.left + insets.right; 750 int height = size.height + insets.top + insets.bottom; 751 return new Dimension(width, height); 752 } 753 754 @Override 755public Dimension preferredLayoutSize(Container parent) { 756 Dimension size = preferredNodeSize(getModel()); 757 return sizeWithInsets(parent, size); 758 } 759 760 @Override 761public Dimension minimumLayoutSize(Container parent) { 762 Dimension size = minimumNodeSize(getModel()); 763 return sizeWithInsets(parent, size); 764 } 765 766 767 private Rectangle boundsWithYandHeight(Rectangle bounds, double y, double height) { 768 Rectangle r = new Rectangle(); 769 r.setBounds((int)(bounds.getX()), (int)y, (int)(bounds.getWidth()), (int)height); 770 return r; 771 } 772 773 private Rectangle boundsWithXandWidth(Rectangle bounds, double x, double width) { 774 Rectangle r = new Rectangle(); 775 r.setBounds((int)x, (int)(bounds.getY()), (int)width, (int)(bounds.getHeight())); 776 return r; 777 } 778 779 780 private void minimizeSplitBounds(Split split, Rectangle bounds) { 781 assert ( split.isVisible()); 782 Rectangle splitBounds = new Rectangle(bounds.x, bounds.y, 0, 0); 783 List<Node> splitChildren = split.getChildren(); 784 Node lastChild = null; 785 int lastVisibleChildIdx = splitChildren.size(); 786 do { 787 lastVisibleChildIdx--; 788 lastChild = splitChildren.get( lastVisibleChildIdx ); 789 } while (( lastVisibleChildIdx > 0 ) && !lastChild.isVisible()); 790 791 if ( !lastChild.isVisible()) 792 return; 793 if ( lastVisibleChildIdx >= 0 ) { 794 Rectangle lastChildBounds = lastChild.getBounds(); 795 if (split.isRowLayout()) { 796 int lastChildMaxX = lastChildBounds.x + lastChildBounds.width; 797 splitBounds.add(lastChildMaxX, bounds.y + bounds.height); 798 } 799 else { 800 int lastChildMaxY = lastChildBounds.y + lastChildBounds.height; 801 splitBounds.add(bounds.x + bounds.width, lastChildMaxY); 802 } 803 } 804 split.setBounds(splitBounds); 805 } 806 807 808 private void layoutShrink(Split split, Rectangle bounds) { 809 Rectangle splitBounds = split.getBounds(); 810 ListIterator<Node> splitChildren = split.getChildren().listIterator(); 811 Node lastWeightedChild = split.lastWeightedChild(); 812 813 if (split.isRowLayout()) { 814 int totalWidth = 0; // sum of the children's widths 815 int minWeightedWidth = 0; // sum of the weighted childrens' min widths 816 int totalWeightedWidth = 0; // sum of the weighted childrens' widths 817 for(Node splitChild : split.getChildren()) { 818 if ( !splitChild.isVisible()) 819 continue; 820 int nodeWidth = splitChild.getBounds().width; 821 int nodeMinWidth = 0; 822 if (( layoutMode == USER_MIN_SIZE_LAYOUT ) && !( splitChild instanceof Divider )) 823 nodeMinWidth = userMinSize; 824 else if ( layoutMode == DEFAULT_LAYOUT ) 825 nodeMinWidth = Math.min(nodeWidth, minimumNodeSize(splitChild).width); 826 totalWidth += nodeWidth; 827 if (splitChild.getWeight() > 0.0) { 828 minWeightedWidth += nodeMinWidth; 829 totalWeightedWidth += nodeWidth; 830 } 831 } 832 833 double x = bounds.getX(); 834 double extraWidth = splitBounds.getWidth() - bounds.getWidth(); 835 double availableWidth = extraWidth; 836 boolean onlyShrinkWeightedComponents = 837 (totalWeightedWidth - minWeightedWidth) > extraWidth; 838 839 while(splitChildren.hasNext()) { 840 Node splitChild = splitChildren.next(); 841 if ( !splitChild.isVisible()) { 842 if ( splitChildren.hasNext()) 843 splitChildren.next(); 844 continue; 845 } 846 Rectangle splitChildBounds = splitChild.getBounds(); 847 double minSplitChildWidth = 0.0; 848 if (( layoutMode == USER_MIN_SIZE_LAYOUT ) && !( splitChild instanceof Divider )) 849 minSplitChildWidth = userMinSize; 850 else if ( layoutMode == DEFAULT_LAYOUT ) 851 minSplitChildWidth = minimumNodeSize(splitChild).getWidth(); 852 double splitChildWeight = (onlyShrinkWeightedComponents) 853 ? splitChild.getWeight() 854 : (splitChildBounds.getWidth() / totalWidth); 855 856 if (!splitChildren.hasNext()) { 857 double newWidth = Math.max(minSplitChildWidth, bounds.getMaxX() - x); 858 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth); 859 layout2(splitChild, newSplitChildBounds); 860 } 861 if ( splitChild.isVisible()) { 862 if ((availableWidth > 0.0) && (splitChildWeight > 0.0)) { 863 double oldWidth = splitChildBounds.getWidth(); 864 double newWidth; 865 if ( splitChild instanceof Divider ) { 866 newWidth = dividerSize; 867 } 868 else { 869 double allocatedWidth = Math.rint(splitChildWeight * extraWidth); 870 newWidth = Math.max(minSplitChildWidth, oldWidth - allocatedWidth); 871 } 872 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth); 873 layout2(splitChild, newSplitChildBounds); 874 availableWidth -= (oldWidth - splitChild.getBounds().getWidth()); 875 } 876 else { 877 double existingWidth = splitChildBounds.getWidth(); 878 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, existingWidth); 879 layout2(splitChild, newSplitChildBounds); 880 } 881 x = splitChild.getBounds().getMaxX(); 882 } 883 } 884 } 885 886 else { 887 int totalHeight = 0; // sum of the children's heights 888 int minWeightedHeight = 0; // sum of the weighted childrens' min heights 889 int totalWeightedHeight = 0; // sum of the weighted childrens' heights 890 for(Node splitChild : split.getChildren()) { 891 if ( !splitChild.isVisible()) 892 continue; 893 int nodeHeight = splitChild.getBounds().height; 894 int nodeMinHeight = 0; 895 if (( layoutMode == USER_MIN_SIZE_LAYOUT ) && !( splitChild instanceof Divider )) 896 nodeMinHeight = userMinSize; 897 else if ( layoutMode == DEFAULT_LAYOUT ) 898 nodeMinHeight = Math.min(nodeHeight, minimumNodeSize(splitChild).height); 899 totalHeight += nodeHeight; 900 if (splitChild.getWeight() > 0.0) { 901 minWeightedHeight += nodeMinHeight; 902 totalWeightedHeight += nodeHeight; 903 } 904 } 905 906 double y = bounds.getY(); 907 double extraHeight = splitBounds.getHeight() - bounds.getHeight(); 908 double availableHeight = extraHeight; 909 boolean onlyShrinkWeightedComponents = 910 (totalWeightedHeight - minWeightedHeight) > extraHeight; 911 912 while(splitChildren.hasNext()) { 913 Node splitChild = splitChildren.next(); 914 if ( !splitChild.isVisible()) { 915 if ( splitChildren.hasNext()) 916 splitChildren.next(); 917 continue; 918 } 919 Rectangle splitChildBounds = splitChild.getBounds(); 920 double minSplitChildHeight = 0.0; 921 if (( layoutMode == USER_MIN_SIZE_LAYOUT ) && !( splitChild instanceof Divider )) 922 minSplitChildHeight = userMinSize; 923 else if ( layoutMode == DEFAULT_LAYOUT ) 924 minSplitChildHeight = minimumNodeSize(splitChild).getHeight(); 925 double splitChildWeight = (onlyShrinkWeightedComponents) 926 ? splitChild.getWeight() 927 : (splitChildBounds.getHeight() / totalHeight); 928 929 // If this split child is the last visible node it should all the 930 // remaining space 931 if ( !hasMoreVisibleSiblings( splitChild )) { 932 double oldHeight = splitChildBounds.getHeight(); 933 double newHeight; 934 if ( splitChild instanceof Divider ) { 935 newHeight = dividerSize; 936 } 937 else { 938 newHeight = Math.max(minSplitChildHeight, bounds.getMaxY() - y); 939 } 940 Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight); 941 layout2(splitChild, newSplitChildBounds); 942 availableHeight -= (oldHeight - splitChild.getBounds().getHeight()); 943 } 944 else /*if ( splitChild.isVisible()) {*/ 945 if ((availableHeight > 0.0) && (splitChildWeight > 0.0)) { 946 double newHeight; 947 double oldHeight = splitChildBounds.getHeight(); 948 // Prevent the divider from shrinking 949 if ( splitChild instanceof Divider ) { 950 newHeight = dividerSize; 951 } 952 else { 953 double allocatedHeight = Math.rint(splitChildWeight * extraHeight); 954 newHeight = Math.max(minSplitChildHeight, oldHeight - allocatedHeight); 955 } 956 Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight); 957 layout2(splitChild, newSplitChildBounds); 958 availableHeight -= (oldHeight - splitChild.getBounds().getHeight()); 959 } 960 else { 961 double existingHeight = splitChildBounds.getHeight(); 962 Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, existingHeight); 963 layout2(splitChild, newSplitChildBounds); 964 } 965 y = splitChild.getBounds().getMaxY(); 966 } 967 } 968 969 /* The bounds of the Split node root are set to be 970 * big enough to contain all of its children. Since 971 * Leaf children can't be reduced below their 972 * (corresponding java.awt.Component) minimum sizes, 973 * the size of the Split's bounds maybe be larger than 974 * the bounds we were asked to fit within. 975 */ 976 minimizeSplitBounds(split, bounds); 977 } 978 979 /** 980 * Check if the specified node has any following visible siblings 981 * @param splitChild the node to check 982 * @param true if there are visible children following 983 */ 984 private boolean hasMoreVisibleSiblings( Node splitChild ) { 985 Node next = splitChild.nextSibling(); 986 if ( next == null ) 987 return false; 988 989 do { 990 if ( next.isVisible()) 991 return true; 992 next = next.nextSibling(); 993 } while ( next != null ); 994 995 return false; 996 } 997 998 private void layoutGrow(Split split, Rectangle bounds) { 999 Rectangle splitBounds = split.getBounds(); 1000 ListIterator<Node> splitChildren = split.getChildren().listIterator(); 1001 Node lastWeightedChild = split.lastWeightedChild(); 1002 1003 /* Layout the Split's child Nodes' along the X axis. The bounds 1004 * of each child will have the same y coordinate and height as the 1005 * layoutGrow() bounds argument. Extra width is allocated to the 1006 * to each child with a non-zero weight: 1007 * newWidth = currentWidth + (extraWidth * splitChild.getWeight()) 1008 * Any extraWidth "left over" (that's availableWidth in the loop 1009 * below) is given to the last child. Note that Dividers always 1010 * have a weight of zero, and they're never the last child. 1011 */ 1012 if (split.isRowLayout()) { 1013 double x = bounds.getX(); 1014 double extraWidth = bounds.getWidth() - splitBounds.getWidth(); 1015 double availableWidth = extraWidth; 1016 1017 while(splitChildren.hasNext()) { 1018 Node splitChild = splitChildren.next(); 1019 if ( !splitChild.isVisible()) { 1020 continue; 1021 } 1022 Rectangle splitChildBounds = splitChild.getBounds(); 1023 double splitChildWeight = splitChild.getWeight(); 1024 1025 if ( !hasMoreVisibleSiblings( splitChild )) { 1026 double newWidth = bounds.getMaxX() - x; 1027 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth); 1028 layout2(splitChild, newSplitChildBounds); 1029 } 1030 else if ((availableWidth > 0.0) && (splitChildWeight > 0.0)) { 1031 double allocatedWidth = (splitChild.equals(lastWeightedChild)) 1032 ? availableWidth 1033 : Math.rint(splitChildWeight * extraWidth); 1034 double newWidth = splitChildBounds.getWidth() + allocatedWidth; 1035 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth); 1036 layout2(splitChild, newSplitChildBounds); 1037 availableWidth -= allocatedWidth; 1038 } 1039 else { 1040 double existingWidth = splitChildBounds.getWidth(); 1041 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, existingWidth); 1042 layout2(splitChild, newSplitChildBounds); 1043 } 1044 x = splitChild.getBounds().getMaxX(); 1045 } 1046 } 1047 1048 /* Layout the Split's child Nodes' along the Y axis. The bounds 1049 * of each child will have the same x coordinate and width as the 1050 * layoutGrow() bounds argument. Extra height is allocated to the 1051 * to each child with a non-zero weight: 1052 * newHeight = currentHeight + (extraHeight * splitChild.getWeight()) 1053 * Any extraHeight "left over" (that's availableHeight in the loop 1054 * below) is given to the last child. Note that Dividers always 1055 * have a weight of zero, and they're never the last child. 1056 */ 1057 else { 1058 double y = bounds.getY(); 1059 double extraHeight = bounds.getHeight() - splitBounds.getHeight(); 1060 double availableHeight = extraHeight; 1061 1062 while(splitChildren.hasNext()) { 1063 Node splitChild = splitChildren.next(); 1064 if ( !splitChild.isVisible()) { 1065 continue; 1066 } 1067 Rectangle splitChildBounds = splitChild.getBounds(); 1068 double splitChildWeight = splitChild.getWeight(); 1069 1070 if (!splitChildren.hasNext()) { 1071 double newHeight = bounds.getMaxY() - y; 1072 Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight); 1073 layout2(splitChild, newSplitChildBounds); 1074 } 1075 else if ((availableHeight > 0.0) && (splitChildWeight > 0.0)) { 1076 double allocatedHeight = (splitChild.equals(lastWeightedChild)) 1077 ? availableHeight 1078 : Math.rint(splitChildWeight * extraHeight); 1079 double newHeight = splitChildBounds.getHeight() + allocatedHeight; 1080 Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight); 1081 layout2(splitChild, newSplitChildBounds); 1082 availableHeight -= allocatedHeight; 1083 } 1084 else { 1085 double existingHeight = splitChildBounds.getHeight(); 1086 Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, existingHeight); 1087 layout2(splitChild, newSplitChildBounds); 1088 } 1089 y = splitChild.getBounds().getMaxY(); 1090 } 1091 } 1092 } 1093 1094 1095 /* Second pass of the layout algorithm: branch to layoutGrow/Shrink 1096 * as needed. 1097 */ 1098 private void layout2(Node root, Rectangle bounds) { 1099 if (root instanceof Leaf) { 1100 Component child = childForNode(root); 1101 if (child != null) { 1102 child.setBounds(bounds); 1103 } 1104 root.setBounds(bounds); 1105 } 1106 else if (root instanceof Divider) { 1107 root.setBounds(bounds); 1108 } 1109 else if (root instanceof Split) { 1110 Split split = (Split)root; 1111 boolean grow = split.isRowLayout() 1112 ? (split.getBounds().width <= bounds.width) 1113 : (split.getBounds().height <= bounds.height); 1114 if (grow) { 1115 layoutGrow(split, bounds); 1116 root.setBounds(bounds); 1117 } 1118 else { 1119 layoutShrink(split, bounds); 1120 // split.setBounds() called in layoutShrink() 1121 } 1122 } 1123 } 1124 1125 1126 /* First pass of the layout algorithm. 1127 * 1128 * If the Dividers are "floating" then set the bounds of each 1129 * node to accomodate the preferred size of all of the 1130 * Leaf's java.awt.Components. Otherwise, just set the bounds 1131 * of each Leaf/Split node so that it's to the left of (for 1132 * Split.isRowLayout() Split children) or directly above 1133 * the Divider that follows. 1134 * 1135 * This pass sets the bounds of each Node in the layout model. It 1136 * does not resize any of the parent Container's 1137 * (java.awt.Component) children. That's done in the second pass, 1138 * see layoutGrow() and layoutShrink(). 1139 */ 1140 private void layout1(Node root, Rectangle bounds) { 1141 if (root instanceof Leaf) { 1142 root.setBounds(bounds); 1143 } 1144 else if (root instanceof Split) { 1145 Split split = (Split)root; 1146 Iterator<Node> splitChildren = split.getChildren().iterator(); 1147 Rectangle childBounds = null; 1148 int divSize = getDividerSize(); 1149 boolean initSplit = false; 1150 1151 1152 /* Layout the Split's child Nodes' along the X axis. The bounds 1153 * of each child will have the same y coordinate and height as the 1154 * layout1() bounds argument. 1155 * 1156 * Note: the column layout code - that's the "else" clause below 1157 * this if, is identical to the X axis (rowLayout) code below. 1158 */ 1159 if (split.isRowLayout()) { 1160 double x = bounds.getX(); 1161 while(splitChildren.hasNext()) { 1162 Node splitChild = splitChildren.next(); 1163 if ( !splitChild.isVisible()) { 1164 if ( splitChildren.hasNext()) 1165 splitChildren.next(); 1166 continue; 1167 } 1168 Divider dividerChild = 1169 (splitChildren.hasNext()) ? (Divider)(splitChildren.next()) : null; 1170 1171 double childWidth = 0.0; 1172 if (getFloatingDividers()) { 1173 childWidth = preferredNodeSize(splitChild).getWidth(); 1174 } 1175 else { 1176 if ((dividerChild != null) && dividerChild.isVisible()) { 1177 double cw = dividerChild.getBounds().getX() - x; 1178 if ( cw > 0.0 ) 1179 childWidth = cw; 1180 else { 1181 childWidth = preferredNodeSize(splitChild).getWidth(); 1182 initSplit = true; 1183 } 1184 } 1185 else { 1186 childWidth = split.getBounds().getMaxX() - x; 1187 } 1188 } 1189 childBounds = boundsWithXandWidth(bounds, x, childWidth); 1190 layout1(splitChild, childBounds); 1191 1192 if (( initSplit || getFloatingDividers()) && (dividerChild != null) && dividerChild.isVisible()) { 1193 double dividerX = childBounds.getMaxX(); 1194 Rectangle dividerBounds; 1195 dividerBounds = boundsWithXandWidth(bounds, dividerX, divSize); 1196 dividerChild.setBounds(dividerBounds); 1197 } 1198 if ((dividerChild != null) && dividerChild.isVisible()) { 1199 x = dividerChild.getBounds().getMaxX(); 1200 } 1201 } 1202 } 1203 1204 /* Layout the Split's child Nodes' along the Y axis. The bounds 1205 * of each child will have the same x coordinate and width as the 1206 * layout1() bounds argument. The algorithm is identical to what's 1207 * explained above, for the X axis case. 1208 */ 1209 else { 1210 double y = bounds.getY(); 1211 while(splitChildren.hasNext()) { 1212 Node splitChild = splitChildren.next(); 1213 if ( !splitChild.isVisible()) { 1214 if ( splitChildren.hasNext()) 1215 splitChildren.next(); 1216 continue; 1217 } 1218 Divider dividerChild = 1219 (splitChildren.hasNext()) ? (Divider)(splitChildren.next()) : null; 1220 1221 double childHeight = 0.0; 1222 if (getFloatingDividers()) { 1223 childHeight = preferredNodeSize(splitChild).getHeight(); 1224 } 1225 else { 1226 if ((dividerChild != null) && dividerChild.isVisible()) { 1227 double cy = dividerChild.getBounds().getY() - y; 1228 if ( cy > 0.0 ) 1229 childHeight = cy; 1230 else { 1231 childHeight = preferredNodeSize(splitChild).getHeight(); 1232 initSplit = true; 1233 } 1234 } 1235 else { 1236 childHeight = split.getBounds().getMaxY() - y; 1237 } 1238 } 1239 childBounds = boundsWithYandHeight(bounds, y, childHeight); 1240 layout1(splitChild, childBounds); 1241 1242 if (( initSplit || getFloatingDividers()) && (dividerChild != null) && dividerChild.isVisible()) { 1243 double dividerY = childBounds.getMaxY(); 1244 Rectangle dividerBounds = boundsWithYandHeight(bounds, dividerY, divSize); 1245 dividerChild.setBounds(dividerBounds); 1246 } 1247 if ((dividerChild != null) && dividerChild.isVisible()) { 1248 y = dividerChild.getBounds().getMaxY(); 1249 } 1250 } 1251 } 1252 /* The bounds of the Split node root are set to be just 1253 * big enough to contain all of its children, but only 1254 * along the axis it's allocating space on. That's 1255 * X for rows, Y for columns. The second pass of the 1256 * layout algorithm - see layoutShrink()/layoutGrow() 1257 * allocates extra space. 1258 */ 1259 minimizeSplitBounds(split, bounds); 1260 } 1261 } 1262 1263 /** 1264 * Get the layout mode 1265 * @return current layout mode 1266 */ 1267 public int getLayoutMode() 1268 { 1269 return layoutMode; 1270 } 1271 1272 /** 1273 * Set the layout mode. By default this layout uses the preferred and minimum 1274 * sizes of the child components. To ignore the minimum size set the layout 1275 * mode to MultiSplitLayout.LAYOUT_NO_MIN_SIZE. 1276 * @param layoutMode the layout mode 1277 * <ul> 1278 * <li>DEFAULT_LAYOUT - use the preferred and minimum sizes when sizing the children</li> 1279 * <li>LAYOUT_NO_MIN_SIZE - ignore the minimum size when sizing the children</li> 1280 * </li> 1281 */ 1282 public void setLayoutMode( int layoutMode ) 1283 { 1284 this.layoutMode = layoutMode; 1285 } 1286 1287 /** 1288 * Get the minimum node size 1289 * @return the minimum size 1290 */ 1291 public int getUserMinSize() 1292 { 1293 return userMinSize; 1294 } 1295 1296 /** 1297 * Set the user defined minimum size support in the USER_MIN_SIZE_LAYOUT 1298 * layout mode. 1299 * @param minSize the new minimum size 1300 */ 1301 public void setUserMinSize( int minSize ) 1302 { 1303 userMinSize = minSize; 1304 } 1305 1306 /** 1307 * Get the layoutByWeight falg. If the flag is true the layout initializes 1308 * itself using the model weights 1309 * @return the layoutByWeight 1310 */ 1311 public boolean getLayoutByWeight() 1312 { 1313 return layoutByWeight; 1314 } 1315 1316 /** 1317 * Sset the layoutByWeight falg. If the flag is true the layout initializes 1318 * itself using the model weights 1319 * @param state the new layoutByWeight to set 1320 */ 1321 public void setLayoutByWeight( boolean state ) 1322 { 1323 layoutByWeight = state; 1324 } 1325 1326 /** 1327 * The specified Node is either the wrong type or was configured 1328 * incorrectly. 1329 */ 1330 public static class InvalidLayoutException extends RuntimeException { 1331 private final Node node; 1332 public InvalidLayoutException(String msg, Node node) { 1333 super(msg); 1334 this.node = node; 1335 } 1336 /** 1337 * @return the invalid Node. 1338 */ 1339 public Node getNode() { return node; } 1340 } 1341 1342 private void throwInvalidLayout(String msg, Node node) { 1343 throw new InvalidLayoutException(msg, node); 1344 } 1345 1346 private void checkLayout(Node root) { 1347 if (root instanceof Split) { 1348 Split split = (Split)root; 1349 if (split.getChildren().size() <= 2) { 1350 throwInvalidLayout("Split must have > 2 children", root); 1351 } 1352 Iterator<Node> splitChildren = split.getChildren().iterator(); 1353 double weight = 0.0; 1354 while(splitChildren.hasNext()) { 1355 Node splitChild = splitChildren.next(); 1356 if ( !splitChild.isVisible()) { 1357 if ( splitChildren.hasNext()) 1358 splitChildren.next(); 1359 continue; 1360 } 1361 if (splitChild instanceof Divider) { 1362 continue; 1363 //throwInvalidLayout("expected a Split or Leaf Node", splitChild); 1364 } 1365 if (splitChildren.hasNext()) { 1366 Node dividerChild = splitChildren.next(); 1367 if (!(dividerChild instanceof Divider)) { 1368 throwInvalidLayout("expected a Divider Node", dividerChild); 1369 } 1370 } 1371 weight += splitChild.getWeight(); 1372 checkLayout(splitChild); 1373 } 1374 if (weight > 1.0) { 1375 throwInvalidLayout("Split children's total weight > 1.0", root); 1376 } 1377 } 1378 } 1379 1380 /** 1381 * Compute the bounds of all of the Split/Divider/Leaf Nodes in 1382 * the layout model, and then set the bounds of each child component 1383 * with a matching Leaf Node. 1384 */ 1385 @Override 1386public void layoutContainer(Container parent) 1387 { 1388 if ( layoutByWeight && floatingDividers ) 1389 doLayoutByWeight( parent ); 1390 1391 checkLayout(getModel()); 1392 Insets insets = parent.getInsets(); 1393 Dimension size = parent.getSize(); 1394 int width = size.width - (insets.left + insets.right); 1395 int height = size.height - (insets.top + insets.bottom); 1396 Rectangle bounds = new Rectangle( insets.left, insets.top, width, height); 1397 layout1(getModel(), bounds); 1398 layout2(getModel(), bounds); 1399 } 1400 1401 1402 private Divider dividerAt(Node root, int x, int y) { 1403 if (root instanceof Divider) { 1404 Divider divider = (Divider)root; 1405 return (divider.getBounds().contains(x, y)) ? divider : null; 1406 } 1407 else if (root instanceof Split) { 1408 Split split = (Split)root; 1409 for(Node child : split.getChildren()) { 1410 if ( !child.isVisible()) 1411 continue; 1412 if (child.getBounds().contains(x, y)) { 1413 return dividerAt(child, x, y); 1414 } 1415 } 1416 } 1417 return null; 1418 } 1419 1420 /** 1421 * Return the Divider whose bounds contain the specified 1422 * point, or null if there isn't one. 1423 * 1424 * @param x x coordinate 1425 * @param y y coordinate 1426 * @return the Divider at x,y 1427 */ 1428 public Divider dividerAt(int x, int y) { 1429 return dividerAt(getModel(), x, y); 1430 } 1431 1432 private boolean nodeOverlapsRectangle(Node node, Rectangle r2) { 1433 Rectangle r1 = node.getBounds(); 1434 return 1435 (r1.x <= (r2.x + r2.width)) && ((r1.x + r1.width) >= r2.x) && 1436 (r1.y <= (r2.y + r2.height)) && ((r1.y + r1.height) >= r2.y); 1437 } 1438 1439 private List<Divider> dividersThatOverlap(Node root, Rectangle r) { 1440 if (nodeOverlapsRectangle(root, r) && (root instanceof Split)) { 1441 List<Divider> dividers = new ArrayList<Divider>(); 1442 for(Node child : ((Split)root).getChildren()) { 1443 if (child instanceof Divider) { 1444 if (nodeOverlapsRectangle(child, r)) { 1445 dividers.add((Divider)child); 1446 } 1447 } 1448 else if (child instanceof Split) { 1449 dividers.addAll(dividersThatOverlap(child, r)); 1450 } 1451 } 1452 return dividers; 1453 } 1454 else { 1455 return Collections.emptyList(); 1456 } 1457 } 1458 1459 /** 1460 * Return the Dividers whose bounds overlap the specified 1461 * Rectangle. 1462 * 1463 * @param r target Rectangle 1464 * @return the Dividers that overlap r 1465 * @throws IllegalArgumentException if the Rectangle is null 1466 */ 1467 public List<Divider> dividersThatOverlap(Rectangle r) { 1468 if (r == null) { 1469 throw new IllegalArgumentException("null Rectangle"); 1470 } 1471 return dividersThatOverlap(getModel(), r); 1472 } 1473 1474 1475 /** 1476 * Base class for the nodes that model a MultiSplitLayout. 1477 */ 1478 public static abstract class Node implements Serializable { 1479 private Split parent = null; 1480 private Rectangle bounds = new Rectangle(); 1481 private double weight = 0.0; 1482 private boolean isVisible = true; 1483 public void setVisible( boolean b ) { 1484 isVisible = b; 1485 } 1486 1487 /** 1488 * Determines whether this node should be visible when its 1489 * parent is visible. Nodes are 1490 * initially visible 1491 * @return <code>true</code> if the node is visible, 1492 * <code>false</code> otherwise 1493 */ 1494 public boolean isVisible() { 1495 return isVisible; 1496 } 1497 1498 /** 1499 * Returns the Split parent of this Node, or null. 1500 * 1501 * @return the value of the parent property. 1502 * @see #setParent 1503 */ 1504 public Split getParent() { return parent; } 1505 1506 /** 1507 * Set the value of this Node's parent property. The default 1508 * value of this property is null. 1509 * 1510 * @param parent a Split or null 1511 * @see #getParent 1512 */ 1513 public void setParent(Split parent) { 1514 this.parent = parent; 1515 } 1516 1517 /** 1518 * Returns the bounding Rectangle for this Node. 1519 * 1520 * @return the value of the bounds property. 1521 * @see #setBounds 1522 */ 1523 public Rectangle getBounds() { 1524 return new Rectangle(this.bounds); 1525 } 1526 1527 /** 1528 * Set the bounding Rectangle for this node. The value of 1529 * bounds may not be null. The default value of bounds 1530 * is equal to <code>new Rectangle(0,0,0,0)</code>. 1531 * 1532 * @param bounds the new value of the bounds property 1533 * @throws IllegalArgumentException if bounds is null 1534 * @see #getBounds 1535 */ 1536 public void setBounds(Rectangle bounds) { 1537 if (bounds == null) { 1538 throw new IllegalArgumentException("null bounds"); 1539 } 1540 this.bounds = new Rectangle(bounds); 1541 } 1542 1543 /** 1544 * Value between 0.0 and 1.0 used to compute how much space 1545 * to add to this sibling when the layout grows or how 1546 * much to reduce when the layout shrinks. 1547 * 1548 * @return the value of the weight property 1549 * @see #setWeight 1550 */ 1551 public double getWeight() { return weight; } 1552 1553 /** 1554 * The weight property is a between 0.0 and 1.0 used to 1555 * compute how much space to add to this sibling when the 1556 * layout grows or how much to reduce when the layout shrinks. 1557 * If rowLayout is true then this node's width grows 1558 * or shrinks by (extraSpace * weight). If rowLayout is false, 1559 * then the node's height is changed. The default value 1560 * of weight is 0.0. 1561 * 1562 * @param weight a double between 0.0 and 1.0 1563 * @see #getWeight 1564 * @see MultiSplitLayout#layoutContainer 1565 * @throws IllegalArgumentException if weight is not between 0.0 and 1.0 1566 */ 1567 public void setWeight(double weight) { 1568 if ((weight < 0.0)|| (weight > 1.0)) { 1569 throw new IllegalArgumentException("invalid weight"); 1570 } 1571 this.weight = weight; 1572 } 1573 1574 private Node siblingAtOffset(int offset) { 1575 Split p = getParent(); 1576 if (p == null) { return null; } 1577 List<Node> siblings = p.getChildren(); 1578 int index = siblings.indexOf(this); 1579 if (index == -1) { return null; } 1580 index += offset; 1581 return ((index > -1) && (index < siblings.size())) ? siblings.get(index) : null; 1582 } 1583 1584 /** 1585 * Return the Node that comes after this one in the parent's 1586 * list of children, or null. If this node's parent is null, 1587 * or if it's the last child, then return null. 1588 * 1589 * @return the Node that comes after this one in the parent's list of children. 1590 * @see #previousSibling 1591 * @see #getParent 1592 */ 1593 public Node nextSibling() { 1594 return siblingAtOffset(+1); 1595 } 1596 1597 /** 1598 * Return the Node that comes before this one in the parent's 1599 * list of children, or null. If this node's parent is null, 1600 * or if it's the last child, then return null. 1601 * 1602 * @return the Node that comes before this one in the parent's list of children. 1603 * @see #nextSibling 1604 * @see #getParent 1605 */ 1606 public Node previousSibling() { 1607 return siblingAtOffset(-1); 1608 } 1609 } 1610 1611 public static class RowSplit extends Split { 1612 public RowSplit() { 1613 } 1614 1615 public RowSplit(Node... children) { 1616 setChildren(children); 1617 } 1618 1619 /** 1620 * Returns true if the this Split's children are to be 1621 * laid out in a row: all the same height, left edge 1622 * equal to the previous Node's right edge. If false, 1623 * children are laid on in a column. 1624 * 1625 * @return the value of the rowLayout property. 1626 * @see #setRowLayout 1627 */ 1628 @Override 1629 public final boolean isRowLayout() { return true; } 1630 } 1631 1632 public static class ColSplit extends Split { 1633 public ColSplit() { 1634 } 1635 1636 public ColSplit(Node... children) { 1637 setChildren(children); 1638 } 1639 1640 /** 1641 * Returns true if the this Split's children are to be 1642 * laid out in a row: all the same height, left edge 1643 * equal to the previous Node's right edge. If false, 1644 * children are laid on in a column. 1645 * 1646 * @return the value of the rowLayout property. 1647 * @see #setRowLayout 1648 */ 1649 @Override 1650 public final boolean isRowLayout() { return false; } 1651 } 1652 1653 /** 1654 * Defines a vertical or horizontal subdivision into two or more 1655 * tiles. 1656 */ 1657 public static class Split extends Node { 1658 private List<Node> children = Collections.emptyList(); 1659 private boolean rowLayout = true; 1660 private String name; 1661 1662 public Split(Node... children) { 1663 setChildren(children); 1664 } 1665 1666 /** 1667 * Default constructor to support xml (de)serialization and other bean spec dependent ops. 1668 * Resulting instance of Split is invalid until setChildren() is called. 1669 */ 1670 public Split() { 1671 } 1672 1673 /** 1674 * Determines whether this node should be visible when its 1675 * parent is visible. Nodes are 1676 * initially visible 1677 * @return <code>true</code> if the node is visible, 1678 * <code>false</code> otherwise 1679 */ 1680 @Override 1681 public boolean isVisible() { 1682 for(Node child : children) { 1683 if ( child.isVisible() && !( child instanceof Divider )) 1684 return true; 1685 } 1686 return false; 1687 } 1688 1689 /** 1690 * Returns true if the this Split's children are to be 1691 * laid out in a row: all the same height, left edge 1692 * equal to the previous Node's right edge. If false, 1693 * children are laid on in a column. 1694 * 1695 * @return the value of the rowLayout property. 1696 * @see #setRowLayout 1697 */ 1698 public boolean isRowLayout() { return rowLayout; } 1699 1700 /** 1701 * Set the rowLayout property. If true, all of this Split's 1702 * children are to be laid out in a row: all the same height, 1703 * each node's left edge equal to the previous Node's right 1704 * edge. If false, children are laid on in a column. Default 1705 * value is true. 1706 * 1707 * @param rowLayout true for horizontal row layout, false for column 1708 * @see #isRowLayout 1709 */ 1710 public void setRowLayout(boolean rowLayout) { 1711 this.rowLayout = rowLayout; 1712 } 1713 1714 /** 1715 * Returns this Split node's children. The returned value 1716 * is not a reference to the Split's internal list of children 1717 * 1718 * @return the value of the children property. 1719 * @see #setChildren 1720 */ 1721 public List<Node> getChildren() { 1722 return new ArrayList<Node>(children); 1723 } 1724 1725 1726 /** 1727 * Remove a node from the layout. Any sibling dividers will also be removed 1728 * @param n the node to be removed 1729 */ 1730 public void remove( Node n ) { 1731 if ( n.nextSibling() instanceof Divider ) 1732 children.remove( n.nextSibling() ); 1733 else if ( n.previousSibling() instanceof Divider ) 1734 children.remove( n.previousSibling() ); 1735 children.remove( n ); 1736 } 1737 1738 /** 1739 * Replace one node with another. This method is used when a child is removed 1740 * from a split and the split is no longer required, in which case the 1741 * remaining node in the child split can replace the split in the parent 1742 * node 1743 * @param target the node being replaced 1744 * @param replacement the replacement node 1745 */ 1746 public void replace( Node target, Node replacement ) { 1747 int idx = children.indexOf( target ); 1748 children.remove( target ); 1749 children.add( idx, replacement ); 1750 1751 replacement.setParent ( this ); 1752 target.setParent( this ); 1753 } 1754 1755 /** 1756 * Change a node to being hidden. Any associated divider nodes are also hidden 1757 * @param target the node to hide 1758 */ 1759 public void hide( Node target ){ 1760 Node next = target.nextSibling(); 1761 if ( next instanceof Divider ) 1762 next.setVisible( false ); 1763 else { 1764 Node prev = target.previousSibling(); 1765 if ( prev instanceof Divider ) 1766 prev.setVisible( false ); 1767 } 1768 target.setVisible( false ); 1769 } 1770 1771 /** 1772 * Check the dividers to ensure that redundant dividers are hidden and do 1773 * not interfere in the layout, for example when all the children of a split 1774 * are hidden (the split is then invisible), so two dividers may otherwise 1775 * appear next to one another. 1776 * @param split the split to check 1777 */ 1778 public void checkDividers( Split split ) { 1779 ListIterator<Node> splitChildren = split.getChildren().listIterator(); 1780 while( splitChildren.hasNext()) { 1781 Node splitChild = splitChildren.next(); 1782 if ( !splitChild.isVisible()) { 1783 continue; 1784 } 1785 else if ( splitChildren.hasNext()) { 1786 Node dividerChild = splitChildren.next(); 1787 if ( dividerChild instanceof Divider ) { 1788 if ( splitChildren.hasNext()) { 1789 Node rightChild = splitChildren.next(); 1790 while ( !rightChild.isVisible()) { 1791 rightChild = rightChild.nextSibling(); 1792 if ( rightChild == null ) { 1793 // No visible right sibling found, so hide the divider 1794 dividerChild.setVisible( false ); 1795 break; 1796 } 1797 } 1798 1799 // A visible child is found but it's a divider and therefore 1800 // we have to visible and adjacent dividers - so we hide one 1801 if (( rightChild != null ) && ( rightChild instanceof Divider )) 1802 dividerChild.setVisible( false ); 1803 } 1804 } 1805 else if (( splitChild instanceof Divider ) && ( dividerChild instanceof Divider )) { 1806 splitChild.setVisible( false ); 1807 } 1808 } 1809 } 1810 } 1811 1812 /** 1813 * Restore any of the hidden dividers that are required to separate visible nodes 1814 * @param split the node to check 1815 */ 1816 public void restoreDividers( Split split ) { 1817 boolean nextDividerVisible = false; 1818 ListIterator<Node> splitChildren = split.getChildren().listIterator(); 1819 while( splitChildren.hasNext()) { 1820 Node splitChild = splitChildren.next(); 1821 if ( splitChild instanceof Divider ) { 1822 Node prev = splitChild.previousSibling(); 1823 if ( prev.isVisible()) { 1824 Node next = splitChild.nextSibling(); 1825 while ( next != null ) { 1826 if ( next.isVisible()) { 1827 splitChild.setVisible( true ); 1828 break; 1829 } 1830 next = next.nextSibling(); 1831 } 1832 } 1833 } 1834 } 1835 if ( split.getParent() != null ) 1836 restoreDividers( split.getParent()); 1837 } 1838 1839 /** 1840 * Set's the children property of this Split node. The parent 1841 * of each new child is set to this Split node, and the parent 1842 * of each old child (if any) is set to null. This method 1843 * defensively copies the incoming List. Default value is 1844 * an empty List. 1845 * 1846 * @param children List of children 1847 * @see #getChildren 1848 * @throws IllegalArgumentException if children is null 1849 */ 1850 public void setChildren(List<Node> children) { 1851 if (children == null) { 1852 throw new IllegalArgumentException("children must be a non-null List"); 1853 } 1854 for(Node child : this.children) { 1855 child.setParent(null); 1856 } 1857 1858 this.children = new ArrayList<Node>(children); 1859 for(Node child : this.children) { 1860 child.setParent(this); 1861 } 1862 } 1863 1864 /** 1865 * Convenience method for setting the children of this Split node. The parent 1866 * of each new child is set to this Split node, and the parent 1867 * of each old child (if any) is set to null. This method 1868 * defensively copies the incoming array. 1869 * 1870 * @param children array of children 1871 * @see #getChildren 1872 * @throws IllegalArgumentException if children is null 1873 */ 1874 public void setChildren(Node... children) { 1875 setChildren(children == null ? null : Arrays.asList(children)); 1876 } 1877 1878 /** 1879 * Convenience method that returns the last child whose weight 1880 * is > 0.0. 1881 * 1882 * @return the last child whose weight is > 0.0. 1883 * @see #getChildren 1884 * @see Node#getWeight 1885 */ 1886 public final Node lastWeightedChild() { 1887 List<Node> kids = getChildren(); 1888 Node weightedChild = null; 1889 for(Node child : kids) { 1890 if ( !child.isVisible()) 1891 continue; 1892 if (child.getWeight() > 0.0) { 1893 weightedChild = child; 1894 } 1895 } 1896 return weightedChild; 1897 } 1898 1899 /** 1900 * Return the Leaf's name. 1901 * 1902 * @return the value of the name property. 1903 * @see #setName 1904 */ 1905 public String getName() { return name; } 1906 1907 /** 1908 * Set the value of the name property. Name may not be null. 1909 * 1910 * @param name value of the name property 1911 * @throws IllegalArgumentException if name is null 1912 */ 1913 public void setName(String name) { 1914 if (name == null) { 1915 throw new IllegalArgumentException("name is null"); 1916 } 1917 this.name = name; 1918 } 1919 1920 @Override 1921 public String toString() { 1922 int nChildren = getChildren().size(); 1923 StringBuffer sb = new StringBuffer("MultiSplitLayout.Split"); 1924 sb.append(" \""); 1925 sb.append(getName()); 1926 sb.append("\""); 1927 sb.append(isRowLayout() ? " ROW [" : " COLUMN ["); 1928 sb.append(nChildren + ((nChildren == 1) ? " child" : " children")); 1929 sb.append("] "); 1930 sb.append(getBounds()); 1931 return sb.toString(); 1932 } 1933 } 1934 1935 1936 /** 1937 * Models a java.awt Component child. 1938 */ 1939 public static class Leaf extends Node { 1940 private String name = ""; 1941 1942 /** 1943 * Create a Leaf node. The default value of name is "". 1944 */ 1945 public Leaf() { } 1946 1947 1948 /** 1949 * Create a Leaf node with the specified name. Name can not 1950 * be null. 1951 * 1952 * @param name value of the Leaf's name property 1953 * @throws IllegalArgumentException if name is null 1954 */ 1955 public Leaf(String name) { 1956 if (name == null) { 1957 throw new IllegalArgumentException("name is null"); 1958 } 1959 this.name = name; 1960 } 1961 1962 /** 1963 * Return the Leaf's name. 1964 * 1965 * @return the value of the name property. 1966 * @see #setName 1967 */ 1968 public String getName() { return name; } 1969 1970 /** 1971 * Set the value of the name property. Name may not be null. 1972 * 1973 * @param name value of the name property 1974 * @throws IllegalArgumentException if name is null 1975 */ 1976 public void setName(String name) { 1977 if (name == null) { 1978 throw new IllegalArgumentException("name is null"); 1979 } 1980 this.name = name; 1981 } 1982 1983 @Override 1984 public String toString() { 1985 StringBuffer sb = new StringBuffer("MultiSplitLayout.Leaf"); 1986 sb.append(" \""); 1987 sb.append(getName()); 1988 sb.append("\""); 1989 sb.append(" weight="); 1990 sb.append(getWeight()); 1991 sb.append(" "); 1992 sb.append(getBounds()); 1993 return sb.toString(); 1994 } 1995 } 1996 1997 1998 /** 1999 * Models a single vertical/horiztonal divider. 2000 */ 2001 public static class Divider extends Node { 2002 /** 2003 * Convenience method, returns true if the Divider's parent 2004 * is a Split row (a Split with isRowLayout() true), false 2005 * otherwise. In other words if this Divider's major axis 2006 * is vertical, return true. 2007 * 2008 * @return true if this Divider is part of a Split row. 2009 */ 2010 public final boolean isVertical() { 2011 Split parent = getParent(); 2012 return (parent != null) ? parent.isRowLayout() : false; 2013 } 2014 2015 /** 2016 * Dividers can't have a weight, they don't grow or shrink. 2017 * @throws UnsupportedOperationException 2018 */ 2019 @Override 2020 public void setWeight(double weight) { 2021 throw new UnsupportedOperationException(); 2022 } 2023 2024 @Override 2025 public String toString() { 2026 return "MultiSplitLayout.Divider " + getBounds().toString(); 2027 } 2028 } 2029 2030 2031 private static void throwParseException(StreamTokenizer st, String msg) throws Exception { 2032 throw new Exception("MultiSplitLayout.parseModel Error: " + msg); 2033 } 2034 2035 private static void parseAttribute(String name, StreamTokenizer st, Node node) throws Exception { 2036 if ((st.nextToken() != '=')) { 2037 throwParseException(st, "expected '=' after " + name); 2038 } 2039 if (name.equalsIgnoreCase("WEIGHT")) { 2040 if (st.nextToken() == StreamTokenizer.TT_NUMBER) { 2041 node.setWeight(st.nval); 2042 } 2043 else { 2044 throwParseException(st, "invalid weight"); 2045 } 2046 } 2047 else if (name.equalsIgnoreCase("NAME")) { 2048 if (st.nextToken() == StreamTokenizer.TT_WORD) { 2049 if (node instanceof Leaf) { 2050 ((Leaf)node).setName(st.sval); 2051 } 2052 else if (node instanceof Split) { 2053 ((Split)node).setName(st.sval); 2054 } 2055 else { 2056 throwParseException(st, "can't specify name for " + node); 2057 } 2058 } 2059 else { 2060 throwParseException(st, "invalid name"); 2061 } 2062 } 2063 else { 2064 throwParseException(st, "unrecognized attribute \"" + name + "\""); 2065 } 2066 } 2067 2068 private static void addSplitChild(Split parent, Node child) { 2069 List<Node> children = new ArrayList<Node>(parent.getChildren()); 2070 if (children.size() == 0) { 2071 children.add(child); 2072 } 2073 else { 2074 children.add(new Divider()); 2075 children.add(child); 2076 } 2077 parent.setChildren(children); 2078 } 2079 2080 private static void parseLeaf(StreamTokenizer st, Split parent) throws Exception { 2081 Leaf leaf = new Leaf(); 2082 int token; 2083 while ((token = st.nextToken()) != StreamTokenizer.TT_EOF) { 2084 if (token == ')') { 2085 break; 2086 } 2087 if (token == StreamTokenizer.TT_WORD) { 2088 parseAttribute(st.sval, st, leaf); 2089 } 2090 else { 2091 throwParseException(st, "Bad Leaf: " + leaf); 2092 } 2093 } 2094 addSplitChild(parent, leaf); 2095 } 2096 2097 private static void parseSplit(StreamTokenizer st, Split parent) throws Exception { 2098 int token; 2099 while ((token = st.nextToken()) != StreamTokenizer.TT_EOF) { 2100 if (token == ')') { 2101 break; 2102 } 2103 else if (token == StreamTokenizer.TT_WORD) { 2104 if (st.sval.equalsIgnoreCase("WEIGHT")) { 2105 parseAttribute(st.sval, st, parent); 2106 } 2107 else if (st.sval.equalsIgnoreCase("NAME")) { 2108 parseAttribute(st.sval, st, parent); 2109 } 2110 else { 2111 addSplitChild(parent, new Leaf(st.sval)); 2112 } 2113 } 2114 else if (token == '(') { 2115 if ((token = st.nextToken()) != StreamTokenizer.TT_WORD) { 2116 throwParseException(st, "invalid node type"); 2117 } 2118 String nodeType = st.sval.toUpperCase(); 2119 if (nodeType.equals("LEAF")) { 2120 parseLeaf(st, parent); 2121 } 2122 else if (nodeType.equals("ROW") || nodeType.equals("COLUMN")) { 2123 Split split = new Split(); 2124 split.setRowLayout(nodeType.equals("ROW")); 2125 addSplitChild(parent, split); 2126 parseSplit(st, split); 2127 } 2128 else { 2129 throwParseException(st, "unrecognized node type '" + nodeType + "'"); 2130 } 2131 } 2132 } 2133 } 2134 2135 private static Node parseModel(Reader r) { 2136 StreamTokenizer st = new StreamTokenizer(r); 2137 try { 2138 Split root = new Split(); 2139 parseSplit(st, root); 2140 return root.getChildren().get(0); 2141 } 2142 catch (Exception e) { 2143 System.err.println(e); 2144 } 2145 finally { 2146 try { r.close(); } catch (IOException ignore) {} 2147 } 2148 return null; 2149 } 2150 2151 /** 2152 * A convenience method that converts a string to a 2153 * MultiSplitLayout model (a tree of Nodes) using a 2154 * a simple syntax. Nodes are represented by 2155 * parenthetical expressions whose first token 2156 * is one of ROW/COLUMN/LEAF. ROW and COLUMN specify 2157 * horizontal and vertical Split nodes respectively, 2158 * LEAF specifies a Leaf node. A Leaf's name and 2159 * weight can be specified with attributes, 2160 * name=<i>myLeafName</i> weight=<i>myLeafWeight</i>. 2161 * Similarly, a Split's weight can be specified with 2162 * weight=<i>mySplitWeight</i>. 2163 * 2164 * <p> For example, the following expression generates 2165 * a horizontal Split node with three children: 2166 * the Leafs named left and right, and a Divider in 2167 * between: 2168 * <pre> 2169 * (ROW (LEAF name=left) (LEAF name=right weight=1.0)) 2170 * </pre> 2171 * 2172 * <p> Dividers should not be included in the string, 2173 * they're added automatcially as needed. Because 2174 * Leaf nodes often only need to specify a name, one 2175 * can specify a Leaf by just providing the name. 2176 * The previous example can be written like this: 2177 * <pre> 2178 * (ROW left (LEAF name=right weight=1.0)) 2179 * </pre> 2180 * 2181 * <p>Here's a more complex example. One row with 2182 * three elements, the first and last of which are columns 2183 * with two leaves each: 2184 * <pre> 2185 * (ROW (COLUMN weight=0.5 left.top left.bottom) 2186 * (LEAF name=middle) 2187 * (COLUMN weight=0.5 right.top right.bottom)) 2188 * </pre> 2189 * 2190 * 2191 * <p> This syntax is not intended for archiving or 2192 * configuration files . It's just a convenience for 2193 * examples and tests. 2194 * 2195 * @return the Node root of a tree based on s. 2196 */ 2197 public static Node parseModel(String s) { 2198 return parseModel(new StringReader(s)); 2199 } 2200 2201 2202 private static void printModel(String indent, Node root) { 2203 if (root instanceof Split) { 2204 Split split = (Split)root; 2205 System.out.println(indent + split); 2206 for(Node child : split.getChildren()) { 2207 printModel(indent + " ", child); 2208 } 2209 } 2210 else { 2211 System.out.println(indent + root); 2212 } 2213 } 2214 2215 /** 2216 * Print the tree with enough detail for simple debugging. 2217 */ 2218 public static void printModel(Node root) { 2219 printModel("", root); 2220 } 2221}