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.util.ArrayList; 032import java.util.Collections; 033import java.util.HashMap; 034import java.util.Iterator; 035import java.util.List; 036import java.util.ListIterator; 037import java.util.Map; 038 039import javax.swing.UIManager; 040 041import org.openstreetmap.josm.tools.CheckParameterUtil; 042 043/** 044 * The MultiSplitLayout layout manager recursively arranges its 045 * components in row and column groups called "Splits". Elements of 046 * the layout are separated by gaps called "Dividers". The overall 047 * layout is defined with a simple tree model whose nodes are 048 * instances of MultiSplitLayout.Split, MultiSplitLayout.Divider, 049 * and MultiSplitLayout.Leaf. Named Leaf nodes represent the space 050 * allocated to a component that was added with a constraint that 051 * matches the Leaf's name. Extra space is distributed 052 * among row/column siblings according to their 0.0 to 1.0 weight. 053 * If no weights are specified then the last sibling always gets 054 * all of the extra space, or space reduction. 055 * 056 * <p> 057 * Although MultiSplitLayout can be used with any Container, it's 058 * the default layout manager for MultiSplitPane. MultiSplitPane 059 * supports interactively dragging the Dividers, accessibility, 060 * and other features associated with split panes. 061 * 062 * <p> 063 * All properties in this class are bound: when a properties value 064 * is changed, all PropertyChangeListeners are fired. 065 * 066 * @author Hans Muller - SwingX 067 * @see MultiSplitPane 068 */ 069public class MultiSplitLayout implements LayoutManager { 070 private final Map<String, Component> childMap = new HashMap<>(); 071 private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); 072 private Node model; 073 private int dividerSize; 074 private boolean floatingDividers = true; 075 076 /** 077 * Create a MultiSplitLayout with a default model with a single 078 * Leaf node named "default". 079 * 080 * #see setModel 081 */ 082 public MultiSplitLayout() { 083 this(new Leaf("default")); 084 } 085 086 /** 087 * Create a MultiSplitLayout with the specified model. 088 * 089 * #see setModel 090 * @param model model 091 */ 092 public MultiSplitLayout(Node model) { 093 this.model = model; 094 this.dividerSize = UIManager.getInt("SplitPane.dividerSize"); 095 if (this.dividerSize == 0) { 096 this.dividerSize = 7; 097 } 098 } 099 100 /** 101 * Add property change listener. 102 * @param listener listener to add 103 */ 104 public void addPropertyChangeListener(PropertyChangeListener listener) { 105 if (listener != null) { 106 pcs.addPropertyChangeListener(listener); 107 } 108 } 109 110 /** 111 * Remove property change listener. 112 * @param listener listener to remove 113 */ 114 public void removePropertyChangeListener(PropertyChangeListener listener) { 115 if (listener != null) { 116 pcs.removePropertyChangeListener(listener); 117 } 118 } 119 120 /** 121 * Replies list of property change listeners. 122 * @return list of property change listeners 123 */ 124 public PropertyChangeListener[] getPropertyChangeListeners() { 125 return pcs.getPropertyChangeListeners(); 126 } 127 128 private void firePCS(String propertyName, Object oldValue, Object newValue) { 129 if (!(oldValue != null && newValue != null && oldValue.equals(newValue))) { 130 pcs.firePropertyChange(propertyName, oldValue, newValue); 131 } 132 } 133 134 /** 135 * Return the root of the tree of Split, Leaf, and Divider nodes 136 * that define this layout. 137 * 138 * @return the value of the model property 139 * @see #setModel 140 */ 141 public Node getModel() { 142 return model; 143 } 144 145 /** 146 * Set the root of the tree of Split, Leaf, and Divider nodes 147 * that define this layout. The model can be a Split node 148 * (the typical case) or a Leaf. The default value of this 149 * property is a Leaf named "default". 150 * 151 * @param model the root of the tree of Split, Leaf, and Divider node 152 * @throws IllegalArgumentException if model is a Divider or null 153 * @see #getModel 154 */ 155 public void setModel(Node model) { 156 if ((model == null) || (model instanceof Divider)) 157 throw new IllegalArgumentException("invalid model"); 158 Node oldModel = model; 159 this.model = model; 160 firePCS("model", oldModel, model); 161 } 162 163 /** 164 * Returns the width of Dividers in Split rows, and the height of 165 * Dividers in Split columns. 166 * 167 * @return the value of the dividerSize property 168 * @see #setDividerSize 169 */ 170 public int getDividerSize() { 171 return dividerSize; 172 } 173 174 /** 175 * Sets the width of Dividers in Split rows, and the height of 176 * Dividers in Split columns. The default value of this property 177 * is the same as for JSplitPane Dividers. 178 * 179 * @param dividerSize the size of dividers (pixels) 180 * @throws IllegalArgumentException if dividerSize < 0 181 * @see #getDividerSize 182 */ 183 public void setDividerSize(int dividerSize) { 184 if (dividerSize < 0) 185 throw new IllegalArgumentException("invalid dividerSize"); 186 int oldDividerSize = this.dividerSize; 187 this.dividerSize = dividerSize; 188 firePCS("dividerSize", oldDividerSize, dividerSize); 189 } 190 191 /** 192 * @return the value of the floatingDividers property 193 * @see #setFloatingDividers 194 */ 195 public boolean getFloatingDividers() { 196 return floatingDividers; 197 } 198 199 /** 200 * If true, Leaf node bounds match the corresponding component's 201 * preferred size and Splits/Dividers are resized accordingly. 202 * If false then the Dividers define the bounds of the adjacent 203 * Split and Leaf nodes. Typically this property is set to false 204 * after the (MultiSplitPane) user has dragged a Divider. 205 * @param floatingDividers boolean value 206 * 207 * @see #getFloatingDividers 208 */ 209 public void setFloatingDividers(boolean floatingDividers) { 210 boolean oldFloatingDividers = this.floatingDividers; 211 this.floatingDividers = floatingDividers; 212 firePCS("floatingDividers", oldFloatingDividers, floatingDividers); 213 } 214 215 /** 216 * Add a component to this MultiSplitLayout. The 217 * <code>name</code> should match the name property of the Leaf 218 * node that represents the bounds of <code>child</code>. After 219 * layoutContainer() recomputes the bounds of all of the nodes in 220 * the model, it will set this child's bounds to the bounds of the 221 * Leaf node with <code>name</code>. Note: if a component was already 222 * added with the same name, this method does not remove it from 223 * its parent. 224 * 225 * @param name identifies the Leaf node that defines the child's bounds 226 * @param child the component to be added 227 * @see #removeLayoutComponent 228 */ 229 @Override 230 public void addLayoutComponent(String name, Component child) { 231 if (name == null) 232 throw new IllegalArgumentException("name not specified"); 233 childMap.put(name, child); 234 } 235 236 /** 237 * Removes the specified component from the layout. 238 * 239 * @param child the component to be removed 240 * @see #addLayoutComponent 241 */ 242 @Override 243 public void removeLayoutComponent(Component child) { 244 String name = child.getName(); 245 if (name != null) { 246 childMap.remove(name); 247 } 248 } 249 250 private Component childForNode(Node node) { 251 if (node instanceof Leaf) { 252 Leaf leaf = (Leaf) node; 253 String name = leaf.getName(); 254 return (name != null) ? childMap.get(name) : null; 255 } 256 return null; 257 } 258 259 private Dimension preferredComponentSize(Node node) { 260 Component child = childForNode(node); 261 return (child != null) ? child.getPreferredSize() : new Dimension(0, 0); 262 263 } 264 265 private Dimension preferredNodeSize(Node root) { 266 if (root instanceof Leaf) 267 return preferredComponentSize(root); 268 else if (root instanceof Divider) { 269 int dividerSize = getDividerSize(); 270 return new Dimension(dividerSize, dividerSize); 271 } else { 272 Split split = (Split) root; 273 List<Node> splitChildren = split.getChildren(); 274 int width = 0; 275 int height = 0; 276 if (split.isRowLayout()) { 277 for (Node splitChild : splitChildren) { 278 Dimension size = preferredNodeSize(splitChild); 279 width += size.width; 280 height = Math.max(height, size.height); 281 } 282 } else { 283 for (Node splitChild : splitChildren) { 284 Dimension size = preferredNodeSize(splitChild); 285 width = Math.max(width, size.width); 286 height += size.height; 287 } 288 } 289 return new Dimension(width, height); 290 } 291 } 292 293 private Dimension minimumNodeSize(Node root) { 294 if (root instanceof Leaf) { 295 Component child = childForNode(root); 296 return (child != null) ? child.getMinimumSize() : new Dimension(0, 0); 297 } else if (root instanceof Divider) { 298 int dividerSize = getDividerSize(); 299 return new Dimension(dividerSize, dividerSize); 300 } else { 301 Split split = (Split) root; 302 List<Node> splitChildren = split.getChildren(); 303 int width = 0; 304 int height = 0; 305 if (split.isRowLayout()) { 306 for (Node splitChild : splitChildren) { 307 Dimension size = minimumNodeSize(splitChild); 308 width += size.width; 309 height = Math.max(height, size.height); 310 } 311 } else { 312 for (Node splitChild : splitChildren) { 313 Dimension size = minimumNodeSize(splitChild); 314 width = Math.max(width, size.width); 315 height += size.height; 316 } 317 } 318 return new Dimension(width, height); 319 } 320 } 321 322 private static Dimension sizeWithInsets(Container parent, Dimension size) { 323 Insets insets = parent.getInsets(); 324 int width = size.width + insets.left + insets.right; 325 int height = size.height + insets.top + insets.bottom; 326 return new Dimension(width, height); 327 } 328 329 @Override 330 public Dimension preferredLayoutSize(Container parent) { 331 Dimension size = preferredNodeSize(getModel()); 332 return sizeWithInsets(parent, size); 333 } 334 335 @Override 336 public Dimension minimumLayoutSize(Container parent) { 337 Dimension size = minimumNodeSize(getModel()); 338 return sizeWithInsets(parent, size); 339 } 340 341 private static Rectangle boundsWithYandHeight(Rectangle bounds, double y, double height) { 342 Rectangle r = new Rectangle(); 343 r.setBounds((int) (bounds.getX()), (int) y, (int) (bounds.getWidth()), (int) height); 344 return r; 345 } 346 347 private static Rectangle boundsWithXandWidth(Rectangle bounds, double x, double width) { 348 Rectangle r = new Rectangle(); 349 r.setBounds((int) x, (int) (bounds.getY()), (int) width, (int) (bounds.getHeight())); 350 return r; 351 } 352 353 private static void minimizeSplitBounds(Split split, Rectangle bounds) { 354 Rectangle splitBounds = new Rectangle(bounds.x, bounds.y, 0, 0); 355 List<Node> splitChildren = split.getChildren(); 356 Node lastChild = splitChildren.get(splitChildren.size() - 1); 357 Rectangle lastChildBounds = lastChild.getBounds(); 358 if (split.isRowLayout()) { 359 int lastChildMaxX = lastChildBounds.x + lastChildBounds.width; 360 splitBounds.add(lastChildMaxX, bounds.y + bounds.height); 361 } else { 362 int lastChildMaxY = lastChildBounds.y + lastChildBounds.height; 363 splitBounds.add(bounds.x + bounds.width, lastChildMaxY); 364 } 365 split.setBounds(splitBounds); 366 } 367 368 private void layoutShrink(Split split, Rectangle bounds) { 369 Rectangle splitBounds = split.getBounds(); 370 ListIterator<Node> splitChildren = split.getChildren().listIterator(); 371 372 if (split.isRowLayout()) { 373 int totalWidth = 0; // sum of the children's widths 374 int minWeightedWidth = 0; // sum of the weighted childrens' min widths 375 int totalWeightedWidth = 0; // sum of the weighted childrens' widths 376 for (Node splitChild : split.getChildren()) { 377 int nodeWidth = splitChild.getBounds().width; 378 int nodeMinWidth = Math.min(nodeWidth, minimumNodeSize(splitChild).width); 379 totalWidth += nodeWidth; 380 if (splitChild.getWeight() > 0.0) { 381 minWeightedWidth += nodeMinWidth; 382 totalWeightedWidth += nodeWidth; 383 } 384 } 385 386 double x = bounds.getX(); 387 double extraWidth = splitBounds.getWidth() - bounds.getWidth(); 388 double availableWidth = extraWidth; 389 boolean onlyShrinkWeightedComponents = 390 (totalWeightedWidth - minWeightedWidth) > extraWidth; 391 392 while (splitChildren.hasNext()) { 393 Node splitChild = splitChildren.next(); 394 Rectangle splitChildBounds = splitChild.getBounds(); 395 double minSplitChildWidth = minimumNodeSize(splitChild).getWidth(); 396 double splitChildWeight = onlyShrinkWeightedComponents 397 ? splitChild.getWeight() 398 : (splitChildBounds.getWidth() / totalWidth); 399 400 if (!splitChildren.hasNext()) { 401 double newWidth = Math.max(minSplitChildWidth, bounds.getMaxX() - x); 402 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth); 403 layout2(splitChild, newSplitChildBounds); 404 } else if ((availableWidth > 0.0) && (splitChildWeight > 0.0)) { 405 double allocatedWidth = Math.rint(splitChildWeight * extraWidth); 406 double oldWidth = splitChildBounds.getWidth(); 407 double newWidth = Math.max(minSplitChildWidth, oldWidth - allocatedWidth); 408 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth); 409 layout2(splitChild, newSplitChildBounds); 410 availableWidth -= (oldWidth - splitChild.getBounds().getWidth()); 411 } else { 412 double existingWidth = splitChildBounds.getWidth(); 413 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, existingWidth); 414 layout2(splitChild, newSplitChildBounds); 415 } 416 x = splitChild.getBounds().getMaxX(); 417 } 418 } else { 419 int totalHeight = 0; // sum of the children's heights 420 int minWeightedHeight = 0; // sum of the weighted childrens' min heights 421 int totalWeightedHeight = 0; // sum of the weighted childrens' heights 422 for (Node splitChild : split.getChildren()) { 423 int nodeHeight = splitChild.getBounds().height; 424 int nodeMinHeight = Math.min(nodeHeight, minimumNodeSize(splitChild).height); 425 totalHeight += nodeHeight; 426 if (splitChild.getWeight() > 0.0) { 427 minWeightedHeight += nodeMinHeight; 428 totalWeightedHeight += nodeHeight; 429 } 430 } 431 432 double y = bounds.getY(); 433 double extraHeight = splitBounds.getHeight() - bounds.getHeight(); 434 double availableHeight = extraHeight; 435 boolean onlyShrinkWeightedComponents = 436 (totalWeightedHeight - minWeightedHeight) > extraHeight; 437 438 while (splitChildren.hasNext()) { 439 Node splitChild = splitChildren.next(); 440 Rectangle splitChildBounds = splitChild.getBounds(); 441 double minSplitChildHeight = minimumNodeSize(splitChild).getHeight(); 442 double splitChildWeight = onlyShrinkWeightedComponents 443 ? splitChild.getWeight() 444 : (splitChildBounds.getHeight() / totalHeight); 445 446 if (!splitChildren.hasNext()) { 447 double oldHeight = splitChildBounds.getHeight(); 448 double newHeight = Math.max(minSplitChildHeight, bounds.getMaxY() - y); 449 Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight); 450 layout2(splitChild, newSplitChildBounds); 451 availableHeight -= (oldHeight - splitChild.getBounds().getHeight()); 452 } else if ((availableHeight > 0.0) && (splitChildWeight > 0.0)) { 453 double allocatedHeight = Math.rint(splitChildWeight * extraHeight); 454 double oldHeight = splitChildBounds.getHeight(); 455 double newHeight = Math.max(minSplitChildHeight, oldHeight - allocatedHeight); 456 Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight); 457 layout2(splitChild, newSplitChildBounds); 458 availableHeight -= (oldHeight - splitChild.getBounds().getHeight()); 459 } else { 460 double existingHeight = splitChildBounds.getHeight(); 461 Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, existingHeight); 462 layout2(splitChild, newSplitChildBounds); 463 } 464 y = splitChild.getBounds().getMaxY(); 465 } 466 } 467 468 /* The bounds of the Split node root are set to be 469 * big enough to contain all of its children. Since 470 * Leaf children can't be reduced below their 471 * (corresponding java.awt.Component) minimum sizes, 472 * the size of the Split's bounds maybe be larger than 473 * the bounds we were asked to fit within. 474 */ 475 minimizeSplitBounds(split, bounds); 476 } 477 478 private void layoutGrow(Split split, Rectangle bounds) { 479 Rectangle splitBounds = split.getBounds(); 480 ListIterator<Node> splitChildren = split.getChildren().listIterator(); 481 Node lastWeightedChild = split.lastWeightedChild(); 482 483 if (split.isRowLayout()) { 484 /* Layout the Split's child Nodes' along the X axis. The bounds 485 * of each child will have the same y coordinate and height as the 486 * layoutGrow() bounds argument. Extra width is allocated to the 487 * to each child with a non-zero weight: 488 * newWidth = currentWidth + (extraWidth * splitChild.getWeight()) 489 * Any extraWidth "left over" (that's availableWidth in the loop 490 * below) is given to the last child. Note that Dividers always 491 * have a weight of zero, and they're never the last child. 492 */ 493 double x = bounds.getX(); 494 double extraWidth = bounds.getWidth() - splitBounds.getWidth(); 495 double availableWidth = extraWidth; 496 497 while (splitChildren.hasNext()) { 498 Node splitChild = splitChildren.next(); 499 Rectangle splitChildBounds = splitChild.getBounds(); 500 double splitChildWeight = splitChild.getWeight(); 501 502 if (!splitChildren.hasNext()) { 503 double newWidth = bounds.getMaxX() - x; 504 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth); 505 layout2(splitChild, newSplitChildBounds); 506 } else if ((availableWidth > 0.0) && (splitChildWeight > 0.0)) { 507 double allocatedWidth = splitChild.equals(lastWeightedChild) 508 ? availableWidth 509 : Math.rint(splitChildWeight * extraWidth); 510 double newWidth = splitChildBounds.getWidth() + allocatedWidth; 511 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth); 512 layout2(splitChild, newSplitChildBounds); 513 availableWidth -= allocatedWidth; 514 } else { 515 double existingWidth = splitChildBounds.getWidth(); 516 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, existingWidth); 517 layout2(splitChild, newSplitChildBounds); 518 } 519 x = splitChild.getBounds().getMaxX(); 520 } 521 } else { 522 /* Layout the Split's child Nodes' along the Y axis. The bounds 523 * of each child will have the same x coordinate and width as the 524 * layoutGrow() bounds argument. Extra height is allocated to the 525 * to each child with a non-zero weight: 526 * newHeight = currentHeight + (extraHeight * splitChild.getWeight()) 527 * Any extraHeight "left over" (that's availableHeight in the loop 528 * below) is given to the last child. Note that Dividers always 529 * have a weight of zero, and they're never the last child. 530 */ 531 double y = bounds.getY(); 532 double extraHeight = bounds.getMaxY() - splitBounds.getHeight(); 533 double availableHeight = extraHeight; 534 535 while (splitChildren.hasNext()) { 536 Node splitChild = splitChildren.next(); 537 Rectangle splitChildBounds = splitChild.getBounds(); 538 double splitChildWeight = splitChild.getWeight(); 539 540 if (!splitChildren.hasNext()) { 541 double newHeight = bounds.getMaxY() - y; 542 Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight); 543 layout2(splitChild, newSplitChildBounds); 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 } 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 } else if (root instanceof Divider) { 573 root.setBounds(bounds); 574 } else if (root instanceof Split) { 575 Split split = (Split) root; 576 boolean grow = split.isRowLayout() 577 ? split.getBounds().width <= bounds.width 578 : (split.getBounds().height <= bounds.height); 579 if (grow) { 580 layoutGrow(split, bounds); 581 root.setBounds(bounds); 582 } else { 583 layoutShrink(split, bounds); 584 // split.setBounds() called in layoutShrink() 585 } 586 } 587 } 588 589 /* First pass of the layout algorithm. 590 * 591 * If the Dividers are "floating" then set the bounds of each 592 * node to accomodate the preferred size of all of the 593 * Leaf's java.awt.Components. Otherwise, just set the bounds 594 * of each Leaf/Split node so that it's to the left of (for 595 * Split.isRowLayout() Split children) or directly above 596 * the Divider that follows. 597 * 598 * This pass sets the bounds of each Node in the layout model. It 599 * does not resize any of the parent Container's 600 * (java.awt.Component) children. That's done in the second pass, 601 * see layoutGrow() and layoutShrink(). 602 */ 603 private void layout1(Node root, Rectangle bounds) { 604 if (root instanceof Leaf) { 605 root.setBounds(bounds); 606 } else if (root instanceof Split) { 607 Split split = (Split) root; 608 Iterator<Node> splitChildren = split.getChildren().iterator(); 609 Rectangle childBounds; 610 int dividerSize = getDividerSize(); 611 612 /* Layout the Split's child Nodes' along the X axis. The bounds 613 * of each child will have the same y coordinate and height as the 614 * layout1() bounds argument. 615 * 616 * Note: the column layout code - that's the "else" clause below 617 * this if, is identical to the X axis (rowLayout) code below. 618 */ 619 if (split.isRowLayout()) { 620 double x = bounds.getX(); 621 while (splitChildren.hasNext()) { 622 Node splitChild = splitChildren.next(); 623 Divider dividerChild = 624 splitChildren.hasNext() ? (Divider) (splitChildren.next()) : null; 625 626 double childWidth; 627 if (getFloatingDividers()) { 628 childWidth = preferredNodeSize(splitChild).getWidth(); 629 } else { 630 if (dividerChild != null) { 631 childWidth = dividerChild.getBounds().getX() - x; 632 } else { 633 childWidth = split.getBounds().getMaxX() - x; 634 } 635 } 636 childBounds = boundsWithXandWidth(bounds, x, childWidth); 637 layout1(splitChild, childBounds); 638 639 if (getFloatingDividers() && (dividerChild != null)) { 640 double dividerX = childBounds.getMaxX(); 641 Rectangle dividerBounds = boundsWithXandWidth(bounds, dividerX, dividerSize); 642 dividerChild.setBounds(dividerBounds); 643 } 644 if (dividerChild != null) { 645 x = dividerChild.getBounds().getMaxX(); 646 } 647 } 648 } else { 649 /* Layout the Split's child Nodes' along the Y axis. The bounds 650 * of each child will have the same x coordinate and width as the 651 * layout1() bounds argument. The algorithm is identical to what's 652 * explained above, for the X axis case. 653 */ 654 double y = bounds.getY(); 655 while (splitChildren.hasNext()) { 656 Node splitChild = splitChildren.next(); 657 Divider dividerChild = 658 splitChildren.hasNext() ? (Divider) splitChildren.next() : null; 659 660 double childHeight; 661 if (getFloatingDividers()) { 662 childHeight = preferredNodeSize(splitChild).getHeight(); 663 } else { 664 if (dividerChild != null) { 665 childHeight = dividerChild.getBounds().getY() - y; 666 } else { 667 childHeight = split.getBounds().getMaxY() - y; 668 } 669 } 670 childBounds = boundsWithYandHeight(bounds, y, childHeight); 671 layout1(splitChild, childBounds); 672 673 if (getFloatingDividers() && (dividerChild != null)) { 674 double dividerY = childBounds.getMaxY(); 675 Rectangle dividerBounds = boundsWithYandHeight(bounds, dividerY, dividerSize); 676 dividerChild.setBounds(dividerBounds); 677 } 678 if (dividerChild != null) { 679 y = dividerChild.getBounds().getMaxY(); 680 } 681 } 682 } 683 /* The bounds of the Split node root are set to be just 684 * big enough to contain all of its children, but only 685 * along the axis it's allocating space on. That's 686 * X for rows, Y for columns. The second pass of the 687 * layout algorithm - see layoutShrink()/layoutGrow() 688 * allocates extra space. 689 */ 690 minimizeSplitBounds(split, bounds); 691 } 692 } 693 694 /** 695 * The specified Node is either the wrong type or was configured incorrectly. 696 */ 697 public static class InvalidLayoutException extends RuntimeException { 698 private final transient Node node; 699 700 /** 701 * Constructs a new {@code InvalidLayoutException}. 702 * @param msg the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method. 703 * @param node node 704 */ 705 public InvalidLayoutException(String msg, Node node) { 706 super(msg); 707 this.node = node; 708 } 709 710 /** 711 * @return the invalid Node. 712 */ 713 public Node getNode() { 714 return node; 715 } 716 } 717 718 private static void throwInvalidLayout(String msg, Node node) { 719 throw new InvalidLayoutException(msg, node); 720 } 721 722 private static void checkLayout(Node root) { 723 if (root instanceof Split) { 724 Split split = (Split) root; 725 if (split.getChildren().size() <= 2) { 726 throwInvalidLayout("Split must have > 2 children", root); 727 } 728 Iterator<Node> splitChildren = split.getChildren().iterator(); 729 double weight = 0.0; 730 while (splitChildren.hasNext()) { 731 Node splitChild = splitChildren.next(); 732 if (splitChild instanceof Divider) { 733 throwInvalidLayout("expected a Split or Leaf Node", splitChild); 734 } 735 if (splitChildren.hasNext()) { 736 Node dividerChild = splitChildren.next(); 737 if (!(dividerChild instanceof Divider)) { 738 throwInvalidLayout("expected a Divider Node", dividerChild); 739 } 740 } 741 weight += splitChild.getWeight(); 742 checkLayout(splitChild); 743 } 744 if (weight > 1.0 + 0.000000001) { /* add some epsilon to a double check */ 745 throwInvalidLayout("Split children's total weight > 1.0", root); 746 } 747 } 748 } 749 750 /** 751 * Compute the bounds of all of the Split/Divider/Leaf Nodes in 752 * the layout model, and then set the bounds of each child component 753 * with a matching Leaf Node. 754 */ 755 @Override 756 public void layoutContainer(Container parent) { 757 checkLayout(getModel()); 758 Insets insets = parent.getInsets(); 759 Dimension size = parent.getSize(); 760 int width = size.width - (insets.left + insets.right); 761 int height = size.height - (insets.top + insets.bottom); 762 Rectangle bounds = new Rectangle(insets.left, insets.top, width, height); 763 layout1(getModel(), bounds); 764 layout2(getModel(), bounds); 765 } 766 767 private static Divider dividerAt(Node root, int x, int y) { 768 if (root instanceof Divider) { 769 Divider divider = (Divider) root; 770 return divider.getBounds().contains(x, y) ? divider : null; 771 } else if (root instanceof Split) { 772 Split split = (Split) root; 773 for (Node child : split.getChildren()) { 774 if (child.getBounds().contains(x, y)) 775 return dividerAt(child, x, y); 776 } 777 } 778 return null; 779 } 780 781 /** 782 * Return the Divider whose bounds contain the specified 783 * point, or null if there isn't one. 784 * 785 * @param x x coordinate 786 * @param y y coordinate 787 * @return the Divider at x,y 788 */ 789 public Divider dividerAt(int x, int y) { 790 return dividerAt(getModel(), x, y); 791 } 792 793 private static boolean nodeOverlapsRectangle(Node node, Rectangle r2) { 794 Rectangle r1 = node.getBounds(); 795 return 796 (r1.x <= (r2.x + r2.width)) && ((r1.x + r1.width) >= r2.x) && 797 (r1.y <= (r2.y + r2.height)) && ((r1.y + r1.height) >= r2.y); 798 } 799 800 private static List<Divider> dividersThatOverlap(Node root, Rectangle r) { 801 if (nodeOverlapsRectangle(root, r) && (root instanceof Split)) { 802 List<Divider> dividers = new ArrayList<>(); 803 for (Node child : ((Split) root).getChildren()) { 804 if (child instanceof Divider) { 805 if (nodeOverlapsRectangle(child, r)) { 806 dividers.add((Divider) child); 807 } 808 } else if (child instanceof Split) { 809 dividers.addAll(dividersThatOverlap(child, r)); 810 } 811 } 812 return dividers; 813 } else 814 return Collections.emptyList(); 815 } 816 817 /** 818 * Return the Dividers whose bounds overlap the specified 819 * Rectangle. 820 * 821 * @param r target Rectangle 822 * @return the Dividers that overlap r 823 * @throws IllegalArgumentException if the Rectangle is null 824 */ 825 public List<Divider> dividersThatOverlap(Rectangle r) { 826 CheckParameterUtil.ensureParameterNotNull(r, "r"); 827 return dividersThatOverlap(getModel(), r); 828 } 829 830 /** 831 * Base class for the nodes that model a MultiSplitLayout. 832 */ 833 public abstract static class Node { 834 private Split parent; 835 private Rectangle bounds = new Rectangle(); 836 private double weight; 837 838 /** 839 * Returns the Split parent of this Node, or null. 840 * 841 * This method isn't called getParent(), in order to avoid problems 842 * with recursive object creation when using XmlDecoder. 843 * 844 * @return the value of the parent property. 845 * @see #parent_set 846 */ 847 public Split parent_get() { 848 return parent; 849 } 850 851 /** 852 * Set the value of this Node's parent property. The default 853 * value of this property is null. 854 * 855 * This method isn't called setParent(), in order to avoid problems 856 * with recursive object creation when using XmlEncoder. 857 * 858 * @param parent a Split or null 859 * @see #parent_get 860 */ 861 public void parent_set(Split parent) { 862 this.parent = parent; 863 } 864 865 /** 866 * Returns the bounding Rectangle for this Node. 867 * 868 * @return the value of the bounds property. 869 * @see #setBounds 870 */ 871 public Rectangle getBounds() { 872 return new Rectangle(this.bounds); 873 } 874 875 /** 876 * Set the bounding Rectangle for this node. The value of 877 * bounds may not be null. The default value of bounds 878 * is equal to <code>new Rectangle(0,0,0,0)</code>. 879 * 880 * @param bounds the new value of the bounds property 881 * @throws IllegalArgumentException if bounds is null 882 * @see #getBounds 883 */ 884 public void setBounds(Rectangle bounds) { 885 CheckParameterUtil.ensureParameterNotNull(bounds, "bounds"); 886 this.bounds = new Rectangle(bounds); 887 } 888 889 /** 890 * Value between 0.0 and 1.0 used to compute how much space 891 * to add to this sibling when the layout grows or how 892 * much to reduce when the layout shrinks. 893 * 894 * @return the value of the weight property 895 * @see #setWeight 896 */ 897 public double getWeight() { 898 return weight; 899 } 900 901 /** 902 * The weight property is a between 0.0 and 1.0 used to 903 * compute how much space to add to this sibling when the 904 * layout grows or how much to reduce when the layout shrinks. 905 * If rowLayout is true then this node's width grows 906 * or shrinks by (extraSpace * weight). If rowLayout is false, 907 * then the node's height is changed. The default value 908 * of weight is 0.0. 909 * 910 * @param weight a double between 0.0 and 1.0 911 * @throws IllegalArgumentException if weight is not between 0.0 and 1.0 912 * @see #getWeight 913 * @see MultiSplitLayout#layoutContainer 914 */ 915 public void setWeight(double weight) { 916 if ((weight < 0.0) || (weight > 1.0)) 917 throw new IllegalArgumentException("invalid weight"); 918 this.weight = weight; 919 } 920 921 private Node siblingAtOffset(int offset) { 922 Split parent = parent_get(); 923 if (parent == null) 924 return null; 925 List<Node> siblings = parent.getChildren(); 926 int index = siblings.indexOf(this); 927 if (index == -1) 928 return null; 929 index += offset; 930 return ((index > -1) && (index < siblings.size())) ? siblings.get(index) : null; 931 } 932 933 /** 934 * Return the Node that comes after 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 after this one in the parent's list of children. 939 * @see #previousSibling 940 * @see #parent_get 941 */ 942 public Node nextSibling() { 943 return siblingAtOffset(+1); 944 } 945 946 /** 947 * Return the Node that comes before this one in the parent's 948 * list of children, or null. If this node's parent is null, 949 * or if it's the last child, then return null. 950 * 951 * @return the Node that comes before this one in the parent's list of children. 952 * @see #nextSibling 953 * @see #parent_get 954 */ 955 public Node previousSibling() { 956 return siblingAtOffset(-1); 957 } 958 } 959 960 /** 961 * Defines a vertical or horizontal subdivision into two or more 962 * tiles. 963 */ 964 public static class Split extends Node { 965 private List<Node> children = Collections.emptyList(); 966 private boolean rowLayout = true; 967 968 /** 969 * Returns true if the this Split's children are to be 970 * laid out in a row: all the same height, left edge 971 * equal to the previous Node's right edge. If false, 972 * children are laid on in a column. 973 * 974 * @return the value of the rowLayout property. 975 * @see #setRowLayout 976 */ 977 public boolean isRowLayout() { 978 return rowLayout; 979 } 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 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 an empty List. 1010 * 1011 * @param children List of children 1012 * @throws IllegalArgumentException if children is null 1013 * @see #getChildren 1014 */ 1015 public void setChildren(List<Node> children) { 1016 if (children == null) 1017 throw new IllegalArgumentException("children must be a non-null List"); 1018 for (Node child : this.children) { 1019 child.parent_set(null); 1020 } 1021 this.children = new ArrayList<>(children); 1022 for (Node child : this.children) { 1023 child.parent_set(this); 1024 } 1025 } 1026 1027 /** 1028 * Convenience method that returns the last child whose weight 1029 * is > 0.0. 1030 * 1031 * @return the last child whose weight is > 0.0. 1032 * @see #getChildren 1033 * @see Node#getWeight 1034 */ 1035 public final Node lastWeightedChild() { 1036 List<Node> children = getChildren(); 1037 Node weightedChild = null; 1038 for (Node child : children) { 1039 if (child.getWeight() > 0.0) { 1040 weightedChild = child; 1041 } 1042 } 1043 return weightedChild; 1044 } 1045 1046 @Override 1047 public String toString() { 1048 int nChildren = getChildren().size(); 1049 StringBuilder sb = new StringBuilder("MultiSplitLayout.Split"); 1050 sb.append(isRowLayout() ? " ROW [" : " COLUMN [") 1051 .append(nChildren + ((nChildren == 1) ? " child" : " children")) 1052 .append("] ") 1053 .append(getBounds()); 1054 return sb.toString(); 1055 } 1056 } 1057 1058 /** 1059 * Models a java.awt Component child. 1060 */ 1061 public static class Leaf extends Node { 1062 private String name = ""; 1063 1064 /** 1065 * Create a Leaf node. The default value of name is "". 1066 */ 1067 public Leaf() { 1068 // Name can be set later with setName() 1069 } 1070 1071 /** 1072 * Create a Leaf node with the specified name. Name can not 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() { 1089 return name; 1090 } 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 CheckParameterUtil.ensureParameterNotNull(name, "name"); 1100 this.name = name; 1101 } 1102 1103 @Override 1104 public String toString() { 1105 return new StringBuilder("MultiSplitLayout.Leaf \"") 1106 .append(getName()) 1107 .append("\" weight=") 1108 .append(getWeight()) 1109 .append(' ') 1110 .append(getBounds()) 1111 .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(); 1130 } 1131 1132 /** 1133 * Dividers can't have a weight, they don't grow or shrink. 1134 * @throws UnsupportedOperationException always 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(); 1144 } 1145 } 1146}