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