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