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