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