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