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