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