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 } else { 248 childMap.values().removeIf(child::equals); 249 } 250 } 251 252 private Component childForNode(Node node) { 253 if (node instanceof Leaf) { 254 Leaf leaf = (Leaf) node; 255 String name = leaf.getName(); 256 return (name != null) ? childMap.get(name) : null; 257 } 258 return null; 259 } 260 261 private Dimension preferredComponentSize(Node node) { 262 Component child = childForNode(node); 263 return (child != null) ? child.getPreferredSize() : new Dimension(0, 0); 264 265 } 266 267 private Dimension preferredNodeSize(Node root) { 268 if (root instanceof Leaf) 269 return preferredComponentSize(root); 270 else if (root instanceof Divider) { 271 int dividerSize = getDividerSize(); 272 return new Dimension(dividerSize, dividerSize); 273 } else { 274 Split split = (Split) root; 275 List<Node> splitChildren = split.getChildren(); 276 int width = 0; 277 int height = 0; 278 if (split.isRowLayout()) { 279 for (Node splitChild : splitChildren) { 280 Dimension size = preferredNodeSize(splitChild); 281 width += size.width; 282 height = Math.max(height, size.height); 283 } 284 } else { 285 for (Node splitChild : splitChildren) { 286 Dimension size = preferredNodeSize(splitChild); 287 width = Math.max(width, size.width); 288 height += size.height; 289 } 290 } 291 return new Dimension(width, height); 292 } 293 } 294 295 private Dimension minimumNodeSize(Node root) { 296 if (root instanceof Leaf) { 297 Component child = childForNode(root); 298 return (child != null) ? child.getMinimumSize() : new Dimension(0, 0); 299 } else if (root instanceof Divider) { 300 int dividerSize = getDividerSize(); 301 return new Dimension(dividerSize, dividerSize); 302 } else { 303 Split split = (Split) root; 304 List<Node> splitChildren = split.getChildren(); 305 int width = 0; 306 int height = 0; 307 if (split.isRowLayout()) { 308 for (Node splitChild : splitChildren) { 309 Dimension size = minimumNodeSize(splitChild); 310 width += size.width; 311 height = Math.max(height, size.height); 312 } 313 } else { 314 for (Node splitChild : splitChildren) { 315 Dimension size = minimumNodeSize(splitChild); 316 width = Math.max(width, size.width); 317 height += size.height; 318 } 319 } 320 return new Dimension(width, height); 321 } 322 } 323 324 private static Dimension sizeWithInsets(Container parent, Dimension size) { 325 Insets insets = parent.getInsets(); 326 int width = size.width + insets.left + insets.right; 327 int height = size.height + insets.top + insets.bottom; 328 return new Dimension(width, height); 329 } 330 331 @Override 332 public Dimension preferredLayoutSize(Container parent) { 333 Dimension size = preferredNodeSize(getModel()); 334 return sizeWithInsets(parent, size); 335 } 336 337 @Override 338 public Dimension minimumLayoutSize(Container parent) { 339 Dimension size = minimumNodeSize(getModel()); 340 return sizeWithInsets(parent, size); 341 } 342 343 private static Rectangle boundsWithYandHeight(Rectangle bounds, double y, double height) { 344 Rectangle r = new Rectangle(); 345 r.setBounds((int) (bounds.getX()), (int) y, (int) (bounds.getWidth()), (int) height); 346 return r; 347 } 348 349 private static Rectangle boundsWithXandWidth(Rectangle bounds, double x, double width) { 350 Rectangle r = new Rectangle(); 351 r.setBounds((int) x, (int) (bounds.getY()), (int) width, (int) (bounds.getHeight())); 352 return r; 353 } 354 355 private static void minimizeSplitBounds(Split split, Rectangle bounds) { 356 Rectangle splitBounds = new Rectangle(bounds.x, bounds.y, 0, 0); 357 List<Node> splitChildren = split.getChildren(); 358 Node lastChild = splitChildren.get(splitChildren.size() - 1); 359 Rectangle lastChildBounds = lastChild.getBounds(); 360 if (split.isRowLayout()) { 361 int lastChildMaxX = lastChildBounds.x + lastChildBounds.width; 362 splitBounds.add(lastChildMaxX, bounds.y + bounds.height); 363 } else { 364 int lastChildMaxY = lastChildBounds.y + lastChildBounds.height; 365 splitBounds.add(bounds.x + bounds.width, lastChildMaxY); 366 } 367 split.setBounds(splitBounds); 368 } 369 370 private void layoutShrink(Split split, Rectangle bounds) { 371 Rectangle splitBounds = split.getBounds(); 372 ListIterator<Node> splitChildren = split.getChildren().listIterator(); 373 374 if (split.isRowLayout()) { 375 int totalWidth = 0; // sum of the children's widths 376 int minWeightedWidth = 0; // sum of the weighted childrens' min widths 377 int totalWeightedWidth = 0; // sum of the weighted childrens' widths 378 for (Node splitChild : split.getChildren()) { 379 int nodeWidth = splitChild.getBounds().width; 380 int nodeMinWidth = Math.min(nodeWidth, minimumNodeSize(splitChild).width); 381 totalWidth += nodeWidth; 382 if (splitChild.getWeight() > 0.0) { 383 minWeightedWidth += nodeMinWidth; 384 totalWeightedWidth += nodeWidth; 385 } 386 } 387 388 double x = bounds.getX(); 389 double extraWidth = splitBounds.getWidth() - bounds.getWidth(); 390 double availableWidth = extraWidth; 391 boolean onlyShrinkWeightedComponents = 392 (totalWeightedWidth - minWeightedWidth) > extraWidth; 393 394 while (splitChildren.hasNext()) { 395 Node splitChild = splitChildren.next(); 396 Rectangle splitChildBounds = splitChild.getBounds(); 397 double minSplitChildWidth = minimumNodeSize(splitChild).getWidth(); 398 double splitChildWeight = onlyShrinkWeightedComponents 399 ? splitChild.getWeight() 400 : (splitChildBounds.getWidth() / totalWidth); 401 402 if (!splitChildren.hasNext()) { 403 double newWidth = Math.max(minSplitChildWidth, bounds.getMaxX() - x); 404 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth); 405 layout2(splitChild, newSplitChildBounds); 406 } else if ((availableWidth > 0.0) && (splitChildWeight > 0.0)) { 407 double allocatedWidth = Math.rint(splitChildWeight * extraWidth); 408 double oldWidth = splitChildBounds.getWidth(); 409 double newWidth = Math.max(minSplitChildWidth, oldWidth - allocatedWidth); 410 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth); 411 layout2(splitChild, newSplitChildBounds); 412 availableWidth -= (oldWidth - splitChild.getBounds().getWidth()); 413 } else { 414 double existingWidth = splitChildBounds.getWidth(); 415 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, existingWidth); 416 layout2(splitChild, newSplitChildBounds); 417 } 418 x = splitChild.getBounds().getMaxX(); 419 } 420 } else { 421 int totalHeight = 0; // sum of the children's heights 422 int minWeightedHeight = 0; // sum of the weighted childrens' min heights 423 int totalWeightedHeight = 0; // sum of the weighted childrens' heights 424 for (Node splitChild : split.getChildren()) { 425 int nodeHeight = splitChild.getBounds().height; 426 int nodeMinHeight = Math.min(nodeHeight, minimumNodeSize(splitChild).height); 427 totalHeight += nodeHeight; 428 if (splitChild.getWeight() > 0.0) { 429 minWeightedHeight += nodeMinHeight; 430 totalWeightedHeight += nodeHeight; 431 } 432 } 433 434 double y = bounds.getY(); 435 double extraHeight = splitBounds.getHeight() - bounds.getHeight(); 436 double availableHeight = extraHeight; 437 boolean onlyShrinkWeightedComponents = 438 (totalWeightedHeight - minWeightedHeight) > extraHeight; 439 440 while (splitChildren.hasNext()) { 441 Node splitChild = splitChildren.next(); 442 Rectangle splitChildBounds = splitChild.getBounds(); 443 double minSplitChildHeight = minimumNodeSize(splitChild).getHeight(); 444 double splitChildWeight = onlyShrinkWeightedComponents 445 ? splitChild.getWeight() 446 : (splitChildBounds.getHeight() / totalHeight); 447 448 if (!splitChildren.hasNext()) { 449 double oldHeight = splitChildBounds.getHeight(); 450 double newHeight = Math.max(minSplitChildHeight, bounds.getMaxY() - y); 451 Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight); 452 layout2(splitChild, newSplitChildBounds); 453 availableHeight -= (oldHeight - splitChild.getBounds().getHeight()); 454 } else if ((availableHeight > 0.0) && (splitChildWeight > 0.0)) { 455 double allocatedHeight = Math.rint(splitChildWeight * extraHeight); 456 double oldHeight = splitChildBounds.getHeight(); 457 double newHeight = Math.max(minSplitChildHeight, oldHeight - allocatedHeight); 458 Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight); 459 layout2(splitChild, newSplitChildBounds); 460 availableHeight -= (oldHeight - splitChild.getBounds().getHeight()); 461 } else { 462 double existingHeight = splitChildBounds.getHeight(); 463 Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, existingHeight); 464 layout2(splitChild, newSplitChildBounds); 465 } 466 y = splitChild.getBounds().getMaxY(); 467 } 468 } 469 470 /* The bounds of the Split node root are set to be 471 * big enough to contain all of its children. Since 472 * Leaf children can't be reduced below their 473 * (corresponding java.awt.Component) minimum sizes, 474 * the size of the Split's bounds maybe be larger than 475 * the bounds we were asked to fit within. 476 */ 477 minimizeSplitBounds(split, bounds); 478 } 479 480 private void layoutGrow(Split split, Rectangle bounds) { 481 Rectangle splitBounds = split.getBounds(); 482 ListIterator<Node> splitChildren = split.getChildren().listIterator(); 483 Node lastWeightedChild = split.lastWeightedChild(); 484 485 if (split.isRowLayout()) { 486 /* Layout the Split's child Nodes' along the X axis. The bounds 487 * of each child will have the same y coordinate and height as the 488 * layoutGrow() bounds argument. Extra width is allocated to the 489 * to each child with a non-zero weight: 490 * newWidth = currentWidth + (extraWidth * splitChild.getWeight()) 491 * Any extraWidth "left over" (that's availableWidth in the loop 492 * below) is given to the last child. Note that Dividers always 493 * have a weight of zero, and they're never the last child. 494 */ 495 double x = bounds.getX(); 496 double extraWidth = bounds.getWidth() - splitBounds.getWidth(); 497 double availableWidth = extraWidth; 498 499 while (splitChildren.hasNext()) { 500 Node splitChild = splitChildren.next(); 501 Rectangle splitChildBounds = splitChild.getBounds(); 502 double splitChildWeight = splitChild.getWeight(); 503 504 if (!splitChildren.hasNext()) { 505 double newWidth = bounds.getMaxX() - x; 506 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth); 507 layout2(splitChild, newSplitChildBounds); 508 } else if ((availableWidth > 0.0) && (splitChildWeight > 0.0)) { 509 double allocatedWidth = splitChild.equals(lastWeightedChild) 510 ? availableWidth 511 : Math.rint(splitChildWeight * extraWidth); 512 double newWidth = splitChildBounds.getWidth() + allocatedWidth; 513 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth); 514 layout2(splitChild, newSplitChildBounds); 515 availableWidth -= allocatedWidth; 516 } else { 517 double existingWidth = splitChildBounds.getWidth(); 518 Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, existingWidth); 519 layout2(splitChild, newSplitChildBounds); 520 } 521 x = splitChild.getBounds().getMaxX(); 522 } 523 } else { 524 /* Layout the Split's child Nodes' along the Y axis. The bounds 525 * of each child will have the same x coordinate and width as the 526 * layoutGrow() bounds argument. Extra height is allocated to the 527 * to each child with a non-zero weight: 528 * newHeight = currentHeight + (extraHeight * splitChild.getWeight()) 529 * Any extraHeight "left over" (that's availableHeight in the loop 530 * below) is given to the last child. Note that Dividers always 531 * have a weight of zero, and they're never the last child. 532 */ 533 double y = bounds.getY(); 534 double extraHeight = bounds.getMaxY() - splitBounds.getHeight(); 535 double availableHeight = extraHeight; 536 537 while (splitChildren.hasNext()) { 538 Node splitChild = splitChildren.next(); 539 Rectangle splitChildBounds = splitChild.getBounds(); 540 double splitChildWeight = splitChild.getWeight(); 541 542 if (!splitChildren.hasNext()) { 543 double newHeight = bounds.getMaxY() - y; 544 Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight); 545 layout2(splitChild, newSplitChildBounds); 546 } else if ((availableHeight > 0.0) && (splitChildWeight > 0.0)) { 547 double allocatedHeight = splitChild.equals(lastWeightedChild) 548 ? availableHeight 549 : Math.rint(splitChildWeight * extraHeight); 550 double newHeight = splitChildBounds.getHeight() + allocatedHeight; 551 Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight); 552 layout2(splitChild, newSplitChildBounds); 553 availableHeight -= allocatedHeight; 554 } else { 555 double existingHeight = splitChildBounds.getHeight(); 556 Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, existingHeight); 557 layout2(splitChild, newSplitChildBounds); 558 } 559 y = splitChild.getBounds().getMaxY(); 560 } 561 } 562 } 563 564 /* Second pass of the layout algorithm: branch to layoutGrow/Shrink 565 * as needed. 566 */ 567 private void layout2(Node root, Rectangle bounds) { 568 if (root instanceof Leaf) { 569 Component child = childForNode(root); 570 if (child != null) { 571 child.setBounds(bounds); 572 } 573 root.setBounds(bounds); 574 } else if (root instanceof Divider) { 575 root.setBounds(bounds); 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 } else { 585 layoutShrink(split, bounds); 586 // split.setBounds() called in layoutShrink() 587 } 588 } 589 } 590 591 /* First pass of the layout algorithm. 592 * 593 * If the Dividers are "floating" then set the bounds of each 594 * node to accomodate the preferred size of all of the 595 * Leaf's java.awt.Components. Otherwise, just set the bounds 596 * of each Leaf/Split node so that it's to the left of (for 597 * Split.isRowLayout() Split children) or directly above 598 * the Divider that follows. 599 * 600 * This pass sets the bounds of each Node in the layout model. It 601 * does not resize any of the parent Container's 602 * (java.awt.Component) children. That's done in the second pass, 603 * see layoutGrow() and layoutShrink(). 604 */ 605 private void layout1(Node root, Rectangle bounds) { 606 if (root instanceof Leaf) { 607 root.setBounds(bounds); 608 } else if (root instanceof Split) { 609 Split split = (Split) root; 610 Iterator<Node> splitChildren = split.getChildren().iterator(); 611 Rectangle childBounds; 612 int dividerSize = getDividerSize(); 613 614 /* Layout the Split's child Nodes' along the X axis. The bounds 615 * of each child will have the same y coordinate and height as the 616 * layout1() bounds argument. 617 * 618 * Note: the column layout code - that's the "else" clause below 619 * this if, is identical to the X axis (rowLayout) code below. 620 */ 621 if (split.isRowLayout()) { 622 double x = bounds.getX(); 623 while (splitChildren.hasNext()) { 624 Node splitChild = splitChildren.next(); 625 Divider dividerChild = null; 626 if (splitChildren.hasNext()) { 627 Node next = splitChildren.next(); 628 if (next instanceof Divider) { 629 dividerChild = (Divider) next; 630 } 631 } 632 633 double childWidth; 634 if (getFloatingDividers()) { 635 childWidth = preferredNodeSize(splitChild).getWidth(); 636 } else { 637 if (dividerChild != null) { 638 childWidth = dividerChild.getBounds().getX() - x; 639 } else { 640 childWidth = split.getBounds().getMaxX() - x; 641 } 642 } 643 childBounds = boundsWithXandWidth(bounds, x, childWidth); 644 layout1(splitChild, childBounds); 645 646 if (getFloatingDividers() && (dividerChild != null)) { 647 double dividerX = childBounds.getMaxX(); 648 Rectangle dividerBounds = boundsWithXandWidth(bounds, dividerX, dividerSize); 649 dividerChild.setBounds(dividerBounds); 650 } 651 if (dividerChild != null) { 652 x = dividerChild.getBounds().getMaxX(); 653 } 654 } 655 } else { 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 double y = bounds.getY(); 662 while (splitChildren.hasNext()) { 663 Node splitChild = splitChildren.next(); 664 Node nodeChild = splitChildren.hasNext() ? splitChildren.next() : null; 665 Divider dividerChild = nodeChild instanceof Divider ? (Divider) nodeChild : null; 666 double childHeight; 667 if (getFloatingDividers()) { 668 childHeight = preferredNodeSize(splitChild).getHeight(); 669 } else { 670 if (dividerChild != null) { 671 childHeight = dividerChild.getBounds().getY() - y; 672 } else { 673 childHeight = split.getBounds().getMaxY() - y; 674 } 675 } 676 childBounds = boundsWithYandHeight(bounds, y, childHeight); 677 layout1(splitChild, childBounds); 678 679 if (getFloatingDividers() && (dividerChild != null)) { 680 double dividerY = childBounds.getMaxY(); 681 Rectangle dividerBounds = boundsWithYandHeight(bounds, dividerY, dividerSize); 682 dividerChild.setBounds(dividerBounds); 683 } 684 if (dividerChild != null) { 685 y = dividerChild.getBounds().getMaxY(); 686 } 687 } 688 } 689 /* The bounds of the Split node root are set to be just 690 * big enough to contain all of its children, but only 691 * along the axis it's allocating space on. That's 692 * X for rows, Y for columns. The second pass of the 693 * layout algorithm - see layoutShrink()/layoutGrow() 694 * allocates extra space. 695 */ 696 minimizeSplitBounds(split, bounds); 697 } 698 } 699 700 /** 701 * The specified Node is either the wrong type or was configured incorrectly. 702 */ 703 public static class InvalidLayoutException extends RuntimeException { 704 private final transient Node node; 705 706 /** 707 * Constructs a new {@code InvalidLayoutException}. 708 * @param msg the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method. 709 * @param node node 710 */ 711 public InvalidLayoutException(String msg, Node node) { 712 super(msg); 713 this.node = node; 714 } 715 716 /** 717 * @return the invalid Node. 718 */ 719 public Node getNode() { 720 return node; 721 } 722 } 723 724 private static void throwInvalidLayout(String msg, Node node) { 725 throw new InvalidLayoutException(msg, node); 726 } 727 728 private static void checkLayout(Node root) { 729 if (root instanceof Split) { 730 Split split = (Split) root; 731 if (split.getChildren().size() <= 2) { 732 throwInvalidLayout("Split must have > 2 children", root); 733 } 734 Iterator<Node> splitChildren = split.getChildren().iterator(); 735 double weight = 0.0; 736 while (splitChildren.hasNext()) { 737 Node splitChild = splitChildren.next(); 738 if (splitChild instanceof Divider) { 739 throwInvalidLayout("expected a Split or Leaf Node", splitChild); 740 } 741 if (splitChildren.hasNext()) { 742 Node dividerChild = splitChildren.next(); 743 if (!(dividerChild instanceof Divider)) { 744 throwInvalidLayout("expected a Divider Node", dividerChild); 745 } 746 } 747 weight += splitChild.getWeight(); 748 checkLayout(splitChild); 749 } 750 if (weight > 1.0 + 0.000000001) { /* add some epsilon to a double check */ 751 throwInvalidLayout("Split children's total weight > 1.0", root); 752 } 753 } 754 } 755 756 /** 757 * Compute the bounds of all of the Split/Divider/Leaf Nodes in 758 * the layout model, and then set the bounds of each child component 759 * with a matching Leaf Node. 760 */ 761 @Override 762 public void layoutContainer(Container parent) { 763 checkLayout(getModel()); 764 Insets insets = parent.getInsets(); 765 Dimension size = parent.getSize(); 766 int width = size.width - (insets.left + insets.right); 767 int height = size.height - (insets.top + insets.bottom); 768 Rectangle bounds = new Rectangle(insets.left, insets.top, width, height); 769 layout1(getModel(), bounds); 770 layout2(getModel(), bounds); 771 } 772 773 private static Divider dividerAt(Node root, int x, int y) { 774 if (root instanceof Divider) { 775 Divider divider = (Divider) root; 776 return divider.getBounds().contains(x, y) ? divider : null; 777 } else if (root instanceof Split) { 778 Split split = (Split) root; 779 for (Node child : split.getChildren()) { 780 if (child.getBounds().contains(x, y)) 781 return dividerAt(child, x, y); 782 } 783 } 784 return null; 785 } 786 787 /** 788 * Return the Divider whose bounds contain the specified 789 * point, or null if there isn't one. 790 * 791 * @param x x coordinate 792 * @param y y coordinate 793 * @return the Divider at x,y 794 */ 795 public Divider dividerAt(int x, int y) { 796 return dividerAt(getModel(), x, y); 797 } 798 799 private static boolean nodeOverlapsRectangle(Node node, Rectangle r2) { 800 Rectangle r1 = node.getBounds(); 801 return 802 (r1.x <= (r2.x + r2.width)) && ((r1.x + r1.width) >= r2.x) && 803 (r1.y <= (r2.y + r2.height)) && ((r1.y + r1.height) >= r2.y); 804 } 805 806 private static List<Divider> dividersThatOverlap(Node root, Rectangle r) { 807 if (nodeOverlapsRectangle(root, r) && (root instanceof Split)) { 808 List<Divider> dividers = new ArrayList<>(); 809 for (Node child : ((Split) root).getChildren()) { 810 if (child instanceof Divider) { 811 if (nodeOverlapsRectangle(child, r)) { 812 dividers.add((Divider) child); 813 } 814 } else if (child instanceof Split) { 815 dividers.addAll(dividersThatOverlap(child, r)); 816 } 817 } 818 return dividers; 819 } else 820 return Collections.emptyList(); 821 } 822 823 /** 824 * Return the Dividers whose bounds overlap the specified 825 * Rectangle. 826 * 827 * @param r target Rectangle 828 * @return the Dividers that overlap r 829 * @throws IllegalArgumentException if the Rectangle is null 830 */ 831 public List<Divider> dividersThatOverlap(Rectangle r) { 832 CheckParameterUtil.ensureParameterNotNull(r, "r"); 833 return dividersThatOverlap(getModel(), r); 834 } 835 836 /** 837 * Base class for the nodes that model a MultiSplitLayout. 838 */ 839 public static class Node { 840 private Split parent; 841 private Rectangle bounds = new Rectangle(); 842 private double weight; 843 844 /** 845 * Constructs a new {@code Node}. 846 */ 847 protected Node() { 848 // Default constructor for subclasses only 849 } 850 851 /** 852 * Returns the Split parent of this Node, or null. 853 * 854 * This method isn't called getParent(), in order to avoid problems 855 * with recursive object creation when using XmlDecoder. 856 * 857 * @return the value of the parent property. 858 * @see #setParent 859 */ 860 public Split getParent() { 861 return parent; 862 } 863 864 /** 865 * Set the value of this Node's parent property. The default 866 * value of this property is null. 867 * 868 * This method isn't called setParent(), in order to avoid problems 869 * with recursive object creation when using XmlEncoder. 870 * 871 * @param parent a Split or null 872 * @see #getParent 873 */ 874 public void setParent(Split parent) { 875 this.parent = parent; 876 } 877 878 /** 879 * Returns the bounding Rectangle for this Node. 880 * 881 * @return the value of the bounds property. 882 * @see #setBounds 883 */ 884 public Rectangle getBounds() { 885 return new Rectangle(this.bounds); 886 } 887 888 /** 889 * Set the bounding Rectangle for this node. The value of 890 * bounds may not be null. The default value of bounds 891 * is equal to <code>new Rectangle(0,0,0,0)</code>. 892 * 893 * @param bounds the new value of the bounds property 894 * @throws IllegalArgumentException if bounds is null 895 * @see #getBounds 896 */ 897 public void setBounds(Rectangle bounds) { 898 CheckParameterUtil.ensureParameterNotNull(bounds, "bounds"); 899 this.bounds = new Rectangle(bounds); 900 } 901 902 /** 903 * Value between 0.0 and 1.0 used to compute how much space 904 * to add to this sibling when the layout grows or how 905 * much to reduce when the layout shrinks. 906 * 907 * @return the value of the weight property 908 * @see #setWeight 909 */ 910 public double getWeight() { 911 return weight; 912 } 913 914 /** 915 * The weight property is a between 0.0 and 1.0 used to 916 * compute how much space to add to this sibling when the 917 * layout grows or how much to reduce when the layout shrinks. 918 * If rowLayout is true then this node's width grows 919 * or shrinks by (extraSpace * weight). If rowLayout is false, 920 * then the node's height is changed. The default value 921 * of weight is 0.0. 922 * 923 * @param weight a double between 0.0 and 1.0 924 * @throws IllegalArgumentException if weight is not between 0.0 and 1.0 925 * @see #getWeight 926 * @see MultiSplitLayout#layoutContainer 927 */ 928 public void setWeight(double weight) { 929 if ((weight < 0.0) || (weight > 1.0)) 930 throw new IllegalArgumentException("invalid weight"); 931 this.weight = weight; 932 } 933 934 private Node siblingAtOffset(int offset) { 935 Split parent = getParent(); 936 if (parent == null) 937 return null; 938 List<Node> siblings = parent.getChildren(); 939 int index = siblings.indexOf(this); 940 if (index == -1) 941 return null; 942 index += offset; 943 return ((index > -1) && (index < siblings.size())) ? siblings.get(index) : null; 944 } 945 946 /** 947 * Return the Node that comes after 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 after this one in the parent's list of children. 952 * @see #previousSibling 953 * @see #getParent 954 */ 955 public Node nextSibling() { 956 return siblingAtOffset(+1); 957 } 958 959 /** 960 * Return the Node that comes before this one in the parent's 961 * list of children, or null. If this node's parent is null, 962 * or if it's the last child, then return null. 963 * 964 * @return the Node that comes before this one in the parent's list of children. 965 * @see #nextSibling 966 * @see #getParent 967 */ 968 public Node previousSibling() { 969 return siblingAtOffset(-1); 970 } 971 } 972 973 /** 974 * Defines a vertical or horizontal subdivision into two or more 975 * tiles. 976 */ 977 public static class Split extends Node { 978 private List<Node> children = Collections.emptyList(); 979 private boolean rowLayout = true; 980 981 /** 982 * Returns true if the this Split's children are to be 983 * laid out in a row: all the same height, left edge 984 * equal to the previous Node's right edge. If false, 985 * children are laid on in a column. 986 * 987 * @return the value of the rowLayout property. 988 * @see #setRowLayout 989 */ 990 public boolean isRowLayout() { 991 return rowLayout; 992 } 993 994 /** 995 * Set the rowLayout property. If true, all of this Split's 996 * children are to be laid out in a row: all the same height, 997 * each node's left edge equal to the previous Node's right 998 * edge. If false, children are laid on in a column. Default value is true. 999 * 1000 * @param rowLayout true for horizontal row layout, false for column 1001 * @see #isRowLayout 1002 */ 1003 public void setRowLayout(boolean rowLayout) { 1004 this.rowLayout = rowLayout; 1005 } 1006 1007 /** 1008 * Returns this Split node's children. The returned value 1009 * is not a reference to the Split's internal list of children 1010 * 1011 * @return the value of the children property. 1012 * @see #setChildren 1013 */ 1014 public List<Node> getChildren() { 1015 return new ArrayList<>(children); 1016 } 1017 1018 /** 1019 * Set's the children property of this Split node. The parent 1020 * of each new child is set to this Split node, and the parent 1021 * of each old child (if any) is set to null. This method 1022 * defensively copies the incoming List. Default value is an empty List. 1023 * 1024 * @param children List of children 1025 * @throws IllegalArgumentException if children is null 1026 * @see #getChildren 1027 */ 1028 public void setChildren(List<Node> children) { 1029 if (children == null) 1030 throw new IllegalArgumentException("children must be a non-null List"); 1031 for (Node child : this.children) { 1032 child.setParent(null); 1033 } 1034 this.children = new ArrayList<>(children); 1035 for (Node child : this.children) { 1036 child.setParent(this); 1037 } 1038 } 1039 1040 /** 1041 * Convenience method that returns the last child whose weight 1042 * is > 0.0. 1043 * 1044 * @return the last child whose weight is > 0.0. 1045 * @see #getChildren 1046 * @see Node#getWeight 1047 */ 1048 public final Node lastWeightedChild() { 1049 List<Node> children = getChildren(); 1050 Node weightedChild = null; 1051 for (Node child : children) { 1052 if (child.getWeight() > 0.0) { 1053 weightedChild = child; 1054 } 1055 } 1056 return weightedChild; 1057 } 1058 1059 @Override 1060 public String toString() { 1061 int nChildren = getChildren().size(); 1062 StringBuilder sb = new StringBuilder("MultiSplitLayout.Split"); 1063 sb.append(isRowLayout() ? " ROW [" : " COLUMN [") 1064 .append(nChildren + ((nChildren == 1) ? " child" : " children")) 1065 .append("] ") 1066 .append(getBounds()); 1067 return sb.toString(); 1068 } 1069 } 1070 1071 /** 1072 * Models a java.awt Component child. 1073 */ 1074 public static class Leaf extends Node { 1075 private String name = ""; 1076 1077 /** 1078 * Create a Leaf node. The default value of name is "". 1079 */ 1080 public Leaf() { 1081 // Name can be set later with setName() 1082 } 1083 1084 /** 1085 * Create a Leaf node with the specified name. Name can not be null. 1086 * 1087 * @param name value of the Leaf's name property 1088 * @throws IllegalArgumentException if name is null 1089 */ 1090 public Leaf(String name) { 1091 CheckParameterUtil.ensureParameterNotNull(name, "name"); 1092 this.name = name; 1093 } 1094 1095 /** 1096 * Return the Leaf's name. 1097 * 1098 * @return the value of the name property. 1099 * @see #setName 1100 */ 1101 public String getName() { 1102 return name; 1103 } 1104 1105 /** 1106 * Set the value of the name property. Name may not be null. 1107 * 1108 * @param name value of the name property 1109 * @throws IllegalArgumentException if name is null 1110 */ 1111 public void setName(String name) { 1112 CheckParameterUtil.ensureParameterNotNull(name, "name"); 1113 this.name = name; 1114 } 1115 1116 @Override 1117 public String toString() { 1118 return new StringBuilder("MultiSplitLayout.Leaf \"") 1119 .append(getName()) 1120 .append("\" weight=") 1121 .append(getWeight()) 1122 .append(' ') 1123 .append(getBounds()) 1124 .toString(); 1125 } 1126 } 1127 1128 /** 1129 * Models a single vertical/horiztonal divider. 1130 */ 1131 public static class Divider extends Node { 1132 /** 1133 * Convenience method, returns true if the Divider's parent 1134 * is a Split row (a Split with isRowLayout() true), false 1135 * otherwise. In other words if this Divider's major axis 1136 * is vertical, return true. 1137 * 1138 * @return true if this Divider is part of a Split row. 1139 */ 1140 public final boolean isVertical() { 1141 Split parent = getParent(); 1142 return parent != null && parent.isRowLayout(); 1143 } 1144 1145 /** 1146 * Dividers can't have a weight, they don't grow or shrink. 1147 * @throws UnsupportedOperationException always 1148 */ 1149 @Override 1150 public void setWeight(double weight) { 1151 throw new UnsupportedOperationException(); 1152 } 1153 1154 @Override 1155 public String toString() { 1156 return "MultiSplitLayout.Divider " + getBounds(); 1157 } 1158 } 1159}