1 /* 2 * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.layout; 27 28 import java.util.ArrayList; 29 import java.util.Collections; 30 import java.util.List; 31 import javafx.beans.property.BooleanProperty; 32 import javafx.beans.property.DoubleProperty; 33 import javafx.beans.property.ObjectProperty; 34 import javafx.css.CssMetaData; 35 import javafx.css.StyleableBooleanProperty; 36 import javafx.css.StyleableDoubleProperty; 37 import javafx.css.StyleableObjectProperty; 38 import javafx.css.StyleableProperty; 39 import javafx.geometry.HPos; 40 import javafx.geometry.Insets; 41 import javafx.geometry.Orientation; 42 import javafx.geometry.Pos; 43 import javafx.geometry.VPos; 44 import javafx.scene.Node; 45 import javafx.css.converter.BooleanConverter; 46 import javafx.css.converter.EnumConverter; 47 import javafx.css.converter.SizeConverter; 48 import javafx.css.Styleable; 49 import javafx.util.Callback; 50 51 /** 52 * VBox lays out its children in a single vertical column. 53 * If the vbox has a border and/or padding set, then the contents will be layed 54 * out within those insets. 55 * <p> 56 * VBox example: 57 * <pre><code> 58 * VBox vbox = new VBox(8); // spacing = 8 59 * vbox.getChildren().addAll(new Button("Cut"), new Button("Copy"), new Button("Paste")); 60 * </code></pre> 61 * 62 * VBox will resize children (if resizable) to their preferred heights and uses its 63 * {@link #fillWidth} property to determine whether to resize their widths to 64 * fill its own width or keep their widths to their preferred (fillWidth defaults to true). 65 * The alignment of the content is controlled by the {@link #alignment} property, 66 * which defaults to Pos.TOP_LEFT. 67 * <p> 68 * If a vbox is resized larger than its preferred height, by default it will keep 69 * children to their preferred heights, leaving the extra space unused. If an 70 * application wishes to have one or more children be allocated that extra space 71 * it may optionally set a vgrow constraint on the child. See "Optional Layout 72 * Constraints" for details. 73 * <p> 74 * VBox lays out each managed child regardless of the child's 75 * visible property value; unmanaged children are ignored.</p> 76 * 77 * <h4>Resizable Range</h4> 78 * 79 * A vbox's parent will resize the vbox within the vbox's resizable range 80 * during layout. By default the vbox computes this range based on its content 81 * as outlined in the table below. 82 * <table border="1"> 83 * <tr><td></td><th>width</th><th>height</th></tr> 84 * <tr><th>minimum</th> 85 * <td>left/right insets plus the largest of the children's min widths.</td> 86 * <td>top/bottom insets plus the sum of each child's min height plus spacing between each child.</td> 87 * </tr> 88 * <tr><th>preferred</th> 89 * <td>left/right insets plus the largest of the children's pref widths.</td> 90 * <td>top/bottom insets plus the sum of each child's pref height plus spacing between each child.</td> 91 * </tr> 92 * <tr><th>maximum</th> 93 * <td>Double.MAX_VALUE</td><td>Double.MAX_VALUE</td></tr> 94 * </table> 95 * <p> 96 * A vbox's unbounded maximum width and height are an indication to the parent that 97 * it may be resized beyond its preferred size to fill whatever space is assigned 98 * to it. 99 * <p> 100 * VBox provides properties for setting the size range directly. These 101 * properties default to the sentinel value USE_COMPUTED_SIZE, however the 102 * application may set them to other values as needed: 103 * <pre><code> 104 * <b>vbox.setPrefWidth(400);</b> 105 * </code></pre> 106 * Applications may restore the computed values by setting these properties back 107 * to USE_COMPUTED_SIZE. 108 * <p> 109 * VBox does not clip its content by default, so it is possible that childrens' 110 * bounds may extend outside its own bounds if a child's min size prevents it from 111 * being fit within the vbox.</p> 112 * 113 * <h4>Optional Layout Constraints</h4> 114 * 115 * An application may set constraints on individual children to customize VBox's layout. 116 * For each constraint, VBox provides a static method for setting it on the child. 117 * <p> 118 * <table border="1"> 119 * <tr><th>Constraint</th><th>Type</th><th>Description</th></tr> 120 * <tr><td>vgrow</td><td>javafx.scene.layout.Priority</td><td>The vertical grow priority for the child.</td></tr> 121 * <tr><td>margin</td><td>javafx.geometry.Insets</td><td>Margin space around the outside of the child.</td></tr> 122 * </table> 123 * <p> 124 * For example, if a vbox needs the ListView to be allocated all extra space: 125 * <pre><code> 126 * VBox vbox = new VBox(); 127 * ListView list = new ListView(); 128 * <b>VBox.setVgrow(list, Priority.ALWAYS);</b> 129 * vbox.getChildren().addAll(new Label("Names:"), list); 130 * </code></pre> 131 * 132 * If more than one child has the same grow priority set, then the vbox will 133 * allocate equal amounts of space to each. VBox will only grow a child up to 134 * its maximum height, so if the child has a max height other than Double.MAX_VALUE, 135 * the application may need to override the max to allow it to grow. 136 * @since JavaFX 2.0 137 */ 138 public class VBox extends Pane { 139 140 private boolean biasDirty = true; 141 private boolean performingLayout = false; 142 private Orientation bias; 143 private double[][] tempArray; 144 145 /******************************************************************** 146 * BEGIN static methods 147 ********************************************************************/ 148 private static final String MARGIN_CONSTRAINT = "vbox-margin"; 149 private static final String VGROW_CONSTRAINT = "vbox-vgrow"; 150 151 /** 152 * Sets the vertical grow priority for the child when contained by an vbox. 153 * If set, the vbox will use the priority to allocate additional space if the 154 * vbox is resized larger than it's preferred height. 155 * If multiple vbox children have the same vertical grow priority, then the 156 * extra space will be split evenly between them. 157 * If no vertical grow priority is set on a child, the vbox will never 158 * allocate it additional vertical space if available. 159 * Setting the value to null will remove the constraint. 160 * @param child the child of a vbox 161 * @param value the horizontal grow priority for the child 162 */ 163 public static void setVgrow(Node child, Priority value) { 164 setConstraint(child, VGROW_CONSTRAINT, value); 165 } 166 167 /** 168 * Returns the child's vgrow property if set. 169 * @param child the child node of a vbox 170 * @return the vertical grow priority for the child or null if no priority was set 171 */ 172 public static Priority getVgrow(Node child) { 173 return (Priority)getConstraint(child, VGROW_CONSTRAINT); 174 } 175 176 /** 177 * Sets the margin for the child when contained by a vbox. 178 * If set, the vbox will layout the child so that it has the margin space around it. 179 * Setting the value to null will remove the constraint. 180 * @param child the child mode of a vbox 181 * @param value the margin of space around the child 182 */ 183 public static void setMargin(Node child, Insets value) { 184 setConstraint(child, MARGIN_CONSTRAINT, value); 185 } 186 187 /** 188 * Returns the child's margin property if set. 189 * @param child the child node of a vbox 190 * @return the margin for the child or null if no margin was set 191 */ 192 public static Insets getMargin(Node child) { 193 return (Insets)getConstraint(child, MARGIN_CONSTRAINT); 194 } 195 196 private static final Callback<Node, Insets> marginAccessor = n -> getMargin(n); 197 198 /** 199 * Removes all vbox constraints from the child node. 200 * @param child the child node 201 */ 202 public static void clearConstraints(Node child) { 203 setVgrow(child, null); 204 setMargin(child, null); 205 } 206 207 /******************************************************************** 208 * END static methods 209 ********************************************************************/ 210 211 /** 212 * Creates a VBox layout with spacing = 0 and alignment at TOP_LEFT. 213 */ 214 public VBox() { 215 super(); 216 } 217 218 /** 219 * Creates a VBox layout with the specified spacing between children. 220 * @param spacing the amount of vertical space between each child 221 */ 222 public VBox(double spacing) { 223 this(); 224 setSpacing(spacing); 225 } 226 227 /** 228 * Creates an VBox layout with spacing = 0. 229 * @param children The initial set of children for this pane. 230 * @since JavaFX 8.0 231 */ 232 public VBox(Node... children) { 233 super(); 234 getChildren().addAll(children); 235 } 236 237 /** 238 * Creates an VBox layout with the specified spacing between children. 239 * @param spacing the amount of horizontal space between each child 240 * @param children The initial set of children for this pane. 241 * @since JavaFX 8.0 242 */ 243 public VBox(double spacing, Node... children) { 244 this(); 245 setSpacing(spacing); 246 getChildren().addAll(children); 247 } 248 249 /** 250 * The amount of vertical space between each child in the vbox. 251 */ 252 public final DoubleProperty spacingProperty() { 253 if (spacing == null) { 254 spacing = new StyleableDoubleProperty() { 255 @Override 256 public void invalidated() { 257 requestLayout(); 258 } 259 260 @Override 261 public Object getBean() { 262 return VBox.this; 263 } 264 265 @Override 266 public String getName() { 267 return "spacing"; 268 } 269 270 @Override 271 public CssMetaData<VBox, Number> getCssMetaData() { 272 return StyleableProperties.SPACING; 273 } 274 }; 275 } 276 return spacing; 277 } 278 279 private DoubleProperty spacing; 280 public final void setSpacing(double value) { spacingProperty().set(value); } 281 public final double getSpacing() { return spacing == null ? 0 : spacing.get(); } 282 283 /** 284 * The overall alignment of children within the vbox's width and height. 285 */ 286 public final ObjectProperty<Pos> alignmentProperty() { 287 if (alignment == null) { 288 alignment = new StyleableObjectProperty<Pos>(Pos.TOP_LEFT) { 289 @Override 290 public void invalidated() { 291 requestLayout(); 292 } 293 294 @Override 295 public Object getBean() { 296 return VBox.this; 297 } 298 299 @Override 300 public String getName() { 301 return "alignment"; 302 } 303 304 @Override 305 public CssMetaData<VBox, Pos> getCssMetaData() { 306 return StyleableProperties.ALIGNMENT; 307 } 308 }; 309 } 310 return alignment; 311 } 312 313 private ObjectProperty<Pos> alignment; 314 public final void setAlignment(Pos value) { alignmentProperty().set(value); } 315 public final Pos getAlignment() { return alignment == null ? Pos.TOP_LEFT : alignment.get(); } 316 private Pos getAlignmentInternal() { 317 Pos localPos = getAlignment(); 318 return localPos == null ? Pos.TOP_LEFT : localPos; 319 } 320 321 /** 322 * Whether or not resizable children will be resized to fill the full width of the vbox 323 * or be resized to their preferred width and aligned according to the <code>alignment</code> 324 * hpos value. 325 */ 326 public final BooleanProperty fillWidthProperty() { 327 if (fillWidth == null) { 328 fillWidth = new StyleableBooleanProperty(true) { 329 @Override 330 public void invalidated() { 331 requestLayout(); 332 } 333 334 @Override 335 public Object getBean() { 336 return VBox.this; 337 } 338 339 @Override 340 public String getName() { 341 return "fillWidth"; 342 } 343 344 @Override 345 public CssMetaData<VBox, Boolean> getCssMetaData() { 346 return StyleableProperties.FILL_WIDTH; 347 } 348 }; 349 } 350 return fillWidth; 351 } 352 353 private BooleanProperty fillWidth; 354 public final void setFillWidth(boolean value) { fillWidthProperty().set(value); } 355 public final boolean isFillWidth() { return fillWidth == null ? true : fillWidth.get(); } 356 357 /** 358 * 359 * @return null unless one of its children has a content bias. 360 */ 361 @Override public Orientation getContentBias() { 362 if (biasDirty) { 363 bias = null; 364 final List<Node> children = getManagedChildren(); 365 for (Node child : children) { 366 Orientation contentBias = child.getContentBias(); 367 if (contentBias != null) { 368 bias = contentBias; 369 if (contentBias == Orientation.HORIZONTAL) { 370 break; 371 } 372 } 373 } 374 biasDirty = false; 375 } 376 return bias; 377 } 378 379 @Override protected double computeMinWidth(double height) { 380 Insets insets = getInsets(); 381 List<Node>managed = getManagedChildren(); 382 double contentWidth = 0; 383 if (height != -1 && getContentBias() != null) { 384 double prefHeights[][] = getAreaHeights(managed, -1, false); 385 adjustAreaHeights(managed, prefHeights, height, -1); 386 contentWidth = computeMaxMinAreaWidth(managed, marginAccessor, prefHeights[0], false); 387 } else { 388 contentWidth = computeMaxMinAreaWidth(managed, marginAccessor); 389 } 390 return snapSpace(insets.getLeft()) + contentWidth + snapSpace(insets.getRight()); 391 } 392 393 @Override protected double computeMinHeight(double width) { 394 Insets insets = getInsets(); 395 return snapSpace(insets.getTop()) + 396 computeContentHeight(getManagedChildren(), width, true) + 397 snapSpace(insets.getBottom()); 398 } 399 400 @Override protected double computePrefWidth(double height) { 401 Insets insets = getInsets(); 402 List<Node>managed = getManagedChildren(); 403 double contentWidth = 0; 404 if (height != -1 && getContentBias() != null) { 405 double prefHeights[][] = getAreaHeights(managed, -1, false); 406 adjustAreaHeights(managed, prefHeights, height, -1); 407 contentWidth = computeMaxPrefAreaWidth(managed, marginAccessor, prefHeights[0], false); 408 } else { 409 contentWidth = computeMaxPrefAreaWidth(managed, marginAccessor); 410 } 411 return snapSpace(insets.getLeft()) + contentWidth + snapSpace(insets.getRight()); 412 } 413 414 @Override protected double computePrefHeight(double width) { 415 Insets insets = getInsets(); 416 double d = snapSpace(insets.getTop()) + 417 computeContentHeight(getManagedChildren(), width, false) + 418 snapSpace(insets.getBottom()); 419 return d; 420 } 421 422 423 private double[][] getAreaHeights(List<Node>managed, double width, boolean minimum) { 424 // width could be -1 425 double[][] temp = getTempArray(managed.size()); 426 final double insideWidth = width == -1? -1 : width - 427 snapSpace(getInsets().getLeft()) - snapSpace(getInsets().getRight()); 428 final boolean isFillWidth = isFillWidth(); 429 for (int i = 0, size = managed.size(); i < size; i++) { 430 Node child = managed.get(i); 431 Insets margin = getMargin(child); 432 if (minimum) { 433 if (insideWidth != -1 && isFillWidth) { 434 temp[0][i] = computeChildMinAreaHeight(child, -1, margin, insideWidth); 435 } else { 436 temp[0][i] = computeChildMinAreaHeight(child, -1, margin, -1); 437 } 438 } else { 439 if (insideWidth != -1 && isFillWidth) { 440 temp[0][i] = computeChildPrefAreaHeight(child, -1, margin, insideWidth); 441 } else { 442 temp[0][i] = computeChildPrefAreaHeight(child, -1, margin, -1); 443 } 444 } 445 } 446 return temp; 447 } 448 449 private double adjustAreaHeights(List<Node>managed, double areaHeights[][], double height, double width) { 450 Insets insets = getInsets(); 451 double left = snapSpace(insets.getLeft()); 452 double right = snapSpace(insets.getRight()); 453 454 double contentHeight = sum(areaHeights[0], managed.size()) + (managed.size()-1)*snapSpace(getSpacing()); 455 double extraHeight = height - 456 snapSpace(insets.getTop()) - snapSpace(insets.getBottom()) - contentHeight; 457 458 if (extraHeight != 0) { 459 final double refWidth = isFillWidth()&& width != -1? width - left - right : -1; 460 double remaining = growOrShrinkAreaHeights(managed, areaHeights, Priority.ALWAYS, extraHeight, refWidth); 461 remaining = growOrShrinkAreaHeights(managed, areaHeights, Priority.SOMETIMES, remaining, refWidth); 462 contentHeight += (extraHeight - remaining); 463 } 464 465 return contentHeight; 466 } 467 468 private double growOrShrinkAreaHeights(List<Node>managed, double areaHeights[][], Priority priority, double extraHeight, double width) { 469 final boolean shrinking = extraHeight < 0; 470 int adjustingNumber = 0; 471 472 double[] usedHeights = areaHeights[0]; 473 double[] temp = areaHeights[1]; 474 475 if (shrinking) { 476 adjustingNumber = managed.size(); 477 for (int i = 0, size = managed.size(); i < size; i++) { 478 final Node child = managed.get(i); 479 temp[i] = computeChildMinAreaHeight(child, -1, getMargin(child), width); 480 } 481 } else { 482 for (int i = 0, size = managed.size(); i < size; i++) { 483 final Node child = managed.get(i); 484 if (getVgrow(child) == priority) { 485 temp[i] = computeChildMaxAreaHeight(child, -1, getMargin(child), width); 486 adjustingNumber++; 487 } else { 488 temp[i] = -1; 489 } 490 } 491 } 492 493 double available = extraHeight; // will be negative in shrinking case 494 outer: while (Math.abs(available) > 1 && adjustingNumber > 0) { 495 final double portion = snapPortion(available / adjustingNumber); // negative in shrinking case 496 for (int i = 0, size = managed.size(); i < size; i++) { 497 if (temp[i] == -1) { 498 continue; 499 } 500 final double limit = temp[i] - usedHeights[i]; // negative in shrinking case 501 final double change = Math.abs(limit) <= Math.abs(portion)? limit : portion; 502 usedHeights[i] += change; 503 available -= change; 504 if (Math.abs(available) < 1) { 505 break outer; 506 } 507 if (Math.abs(change) < Math.abs(portion)) { 508 temp[i] = -1; 509 adjustingNumber--; 510 } 511 } 512 } 513 514 return available; // might be negative in shrinking case 515 } 516 517 private double computeContentHeight(List<Node> managedChildren, double width, boolean minimum) { 518 return sum(getAreaHeights(managedChildren, width, minimum)[0], managedChildren.size()) 519 + (managedChildren.size()-1)*snapSpace(getSpacing()); 520 } 521 522 private static double sum(double[] array, int size) { 523 int i = 0; 524 double res = 0; 525 while (i != size) { 526 res += array[i++]; 527 } 528 return res; 529 } 530 531 @Override public void requestLayout() { 532 if (performingLayout) { 533 return; 534 } 535 biasDirty = true; 536 bias = null; 537 super.requestLayout(); 538 } 539 540 @Override protected void layoutChildren() { 541 performingLayout = true; 542 List<Node> managed = getManagedChildren(); 543 Insets insets = getInsets(); 544 double width = getWidth(); 545 double height = getHeight(); 546 double top = snapSpace(insets.getTop()); 547 double left = snapSpace(insets.getLeft()); 548 double bottom = snapSpace(insets.getBottom()); 549 double right = snapSpace(insets.getRight()); 550 double space = snapSpace(getSpacing()); 551 HPos hpos = getAlignmentInternal().getHpos(); 552 VPos vpos = getAlignmentInternal().getVpos(); 553 boolean isFillWidth = isFillWidth(); 554 555 double[][] actualAreaHeights = getAreaHeights(managed, width, false); 556 double contentWidth = width - left - right; 557 double contentHeight = adjustAreaHeights(managed, actualAreaHeights, height, width); 558 559 double x = left; 560 double y = top + computeYOffset(height - top - bottom, contentHeight, vpos); 561 562 for (int i = 0, size = managed.size(); i < size; i++) { 563 Node child = managed.get(i); 564 layoutInArea(child, x, y, contentWidth, actualAreaHeights[0][i], 565 /* baseline shouldn't matter */actualAreaHeights[0][i], 566 getMargin(child), isFillWidth, true, 567 hpos, vpos); 568 y += actualAreaHeights[0][i] + space; 569 } 570 performingLayout = false; 571 } 572 573 private double[][] getTempArray(int size) { 574 if (tempArray == null) { 575 tempArray = new double[2][size]; // First array for the result, second for temporary computations 576 } else if (tempArray[0].length < size) { 577 tempArray = new double[2][Math.max(tempArray.length * 3, size)]; 578 } 579 return tempArray; 580 581 } 582 583 /*************************************************************************** 584 * * 585 * Stylesheet Handling * 586 * * 587 **************************************************************************/ 588 589 /** 590 * Super-lazy instantiation pattern from Bill Pugh. 591 * @treatAsPrivate implementation detail 592 */ 593 private static class StyleableProperties { 594 private static final CssMetaData<VBox,Pos> ALIGNMENT = 595 new CssMetaData<VBox,Pos>("-fx-alignment", 596 new EnumConverter<Pos>(Pos.class), Pos.TOP_LEFT){ 597 598 @Override 599 public boolean isSettable(VBox node) { 600 return node.alignment == null || !node.alignment.isBound(); 601 } 602 603 @Override 604 public StyleableProperty<Pos> getStyleableProperty(VBox node) { 605 return (StyleableProperty<Pos>)node.alignmentProperty(); 606 } 607 }; 608 609 private static final CssMetaData<VBox,Boolean> FILL_WIDTH = 610 new CssMetaData<VBox,Boolean>("-fx-fill-width", 611 BooleanConverter.getInstance(), Boolean.TRUE) { 612 613 @Override 614 public boolean isSettable(VBox node) { 615 return node.fillWidth == null || !node.fillWidth.isBound(); 616 } 617 618 @Override 619 public StyleableProperty<Boolean> getStyleableProperty(VBox node) { 620 return (StyleableProperty<Boolean>)node.fillWidthProperty(); 621 } 622 }; 623 624 private static final CssMetaData<VBox,Number> SPACING = 625 new CssMetaData<VBox,Number>("-fx-spacing", 626 SizeConverter.getInstance(), 0d) { 627 628 @Override 629 public boolean isSettable(VBox node) { 630 return node.spacing == null || !node.spacing.isBound(); 631 } 632 633 @Override 634 public StyleableProperty<Number> getStyleableProperty(VBox node) { 635 return (StyleableProperty<Number>)node.spacingProperty(); 636 } 637 }; 638 639 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 640 static { 641 final List<CssMetaData<? extends Styleable, ?>> styleables = 642 new ArrayList<CssMetaData<? extends Styleable, ?>>(Region.getClassCssMetaData()); 643 styleables.add(ALIGNMENT); 644 styleables.add(FILL_WIDTH); 645 styleables.add(SPACING); 646 STYLEABLES = Collections.unmodifiableList(styleables); 647 } 648 } 649 650 /** 651 * @return The CssMetaData associated with this class, which may include the 652 * CssMetaData of its super classes. 653 * @since JavaFX 8.0 654 */ 655 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 656 return StyleableProperties.STYLEABLES; 657 } 658 659 /** 660 * {@inheritDoc} 661 * 662 * @since JavaFX 8.0 663 */ 664 665 666 @Override 667 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 668 return getClassCssMetaData(); 669 } 670 671 }