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 com.sun.javafx.binding.ExpressionHelper; 29 import java.util.ArrayList; 30 import java.util.Collections; 31 import java.util.List; 32 import javafx.beans.property.DoubleProperty; 33 import javafx.beans.property.IntegerProperty; 34 import javafx.beans.property.ObjectProperty; 35 import javafx.beans.property.ReadOnlyDoubleProperty; 36 import javafx.css.CssMetaData; 37 import javafx.css.StyleableDoubleProperty; 38 import javafx.css.StyleableIntegerProperty; 39 import javafx.css.StyleableObjectProperty; 40 import javafx.css.StyleableProperty; 41 import javafx.geometry.HPos; 42 import javafx.geometry.Insets; 43 import javafx.geometry.Orientation; 44 import javafx.geometry.Pos; 45 import javafx.geometry.VPos; 46 import javafx.scene.Node; 47 import javafx.css.converter.EnumConverter; 48 import javafx.css.converter.SizeConverter; 49 import javafx.beans.InvalidationListener; 50 import javafx.beans.value.ChangeListener; 51 import javafx.css.Styleable; 52 53 import static javafx.geometry.Orientation.*; 54 import javafx.util.Callback; 55 56 57 /** 58 * TilePane lays out its children in a grid of uniformly sized "tiles". 59 * <p> 60 * A horizontal tilepane (the default) will tile nodes in rows, wrapping at the 61 * tilepane's width. A vertical tilepane will tile nodes in columns, 62 * wrapping at the tilepane's height. 63 * <p> 64 * The size of each "tile" defaults to the size needed to encompass the largest 65 * preferred width and height of the tilepane's children and the tilepane 66 * will recompute the size of the tiles as needed to accommodate the largest preferred 67 * size of its children as it changes. The application may also control the size 68 * of the tiles directly by setting prefTileWidth/prefTileHeight 69 * properties to a value other than USE_COMPUTED_SIZE (the default). 70 * <p> 71 * Applications should initialize either <code>prefColumns</code> (for horizontal) 72 * or <code>prefRows</code> (for vertical) to establish the tilepane's preferred 73 * size (the arbitrary default is 5). Note that prefColumns/prefRows 74 * is used only for calculating the preferred size and may not reflect the actual 75 * number of rows or columns, which may change as the tilepane is resized and 76 * the tiles are wrapped at its actual boundaries. 77 * <p> 78 * The alignment property controls how the rows and columns are aligned 79 * within the bounds of the tilepane and defaults to Pos.TOP_LEFT. It is also possible 80 * to control the alignment of nodes within the individual tiles by setting 81 * {@link #tileAlignmentProperty() tileAlignment}, which defaults to Pos.CENTER. 82 * <p> 83 * A horizontal tilepane example: 84 * <pre>{@code 85 * TilePane tile = new TilePane(); 86 * tile.setHgap(8); 87 * tile.setPrefColumns(4); 88 * for (int i = 0; i < 20; i++) { 89 * tile.getChildren().add(new ImageView(...)); 90 * } 91 * }</pre> 92 * <p> 93 * A vertical TilePane example: 94 * <pre>{@code 95 * TilePane tile = new TilePane(Orientation.VERTICAL); 96 * tile.setTileAlignment(Pos.CENTER_LEFT); 97 * tile.setPrefRows(10); 98 * for (int i = 0; i < 50; i++) { 99 * tile.getChildren().add(new ImageView(...)); 100 * } 101 * }</pre> 102 * 103 * The TilePane will attempt to resize each child to fill its tile. 104 * If the child could not be sized to fill the tile (either because it was not 105 * resizable or its size limits prevented it) then it will be aligned within the 106 * tile using tileAlignment. 107 * 108 * <h3>Resizable Range</h3> 109 * 110 * <p> 111 * A tilepane's parent will resize the tilepane within the tilepane's resizable range 112 * during layout. By default the tilepane computes this range based on its content 113 * as outlined in the tables below. 114 * </p> 115 * <table border="1"> 116 * <caption>Horizontal</caption> 117 * <tr><td></td><th scope="col">width</th><th scope="col">height</th></tr> 118 * <tr><th scope="row">minimum</th> 119 * <td>left/right insets plus the tile width.</td> 120 * <td>top/bottom insets plus height required to display all tiles when wrapped at a specified width with a vgap between each row.</td></tr> 121 * <tr><th scope="row">preferred</th> 122 * <td>left/right insets plus prefColumns multiplied by the tile width.</td> 123 * <td>top/bottom insets plus height required to display all tiles when wrapped at a specified width with a vgap between each row.</td></tr> 124 * <tr><th scope="row">maximum</th> 125 * <td>Double.MAX_VALUE</td><td>Double.MAX_VALUE</td></tr> 126 * </table> 127 * <br> 128 * <table border="1"> 129 * <caption>Vertical</caption> 130 * <tr><td></td><th scope="col">width</th><th scope="col">height</th></tr> 131 * <tr><th scope="row">minimum</th> 132 * <td>left/right insets plus width required to display all tiles when wrapped at a specified height with an hgap between each column.</td> 133 * <td>top/bottom insets plus the tile height.</td></tr> 134 * <tr><th scope="row">preferred</th> 135 * <td>left/right insets plus width required to display all tiles when wrapped at the specified height with an hgap between each column.</td> 136 * <td>top/bottom insets plus prefRows multiplied by the tile height.</td></tr> 137 * <tr><th scope="row">maximum</th> 138 * <td>Double.MAX_VALUE</td><td>Double.MAX_VALUE</td></tr> 139 * </table> 140 * <p> 141 * A tilepane's unbounded maximum width and height are an indication to the parent that 142 * it may be resized beyond its preferred size to fill whatever space is assigned to it. 143 * <p> 144 * TilePane provides properties for setting the size range directly. These 145 * properties default to the sentinel value Region.USE_COMPUTED_SIZE, however the 146 * application may set them to other values as needed: 147 * <pre><code> 148 * <b>tilePane.setMaxWidth(500);</b> 149 * </code></pre> 150 * Applications may restore the computed values by setting these properties back 151 * to Region.USE_COMPUTED_SIZE. 152 * <p> 153 * TilePane does not clip its content by default, so it is possible that children's' 154 * bounds may extend outside the tiles (and possibly the tilepane bounds) if a 155 * child's pref size prevents it from being fit within its tile. Also, if the tilepane 156 * is resized smaller than its preferred size, it may not be able to fit all the 157 * tiles within its bounds and the content will extend outside. 158 * 159 * <h3>Optional Layout Constraints</h3> 160 * 161 * <p> 162 * An application may set constraints on individual children to customize TilePane's layout. 163 * For each constraint, TilePane provides a static method for setting it on the child. 164 * </p> 165 * 166 * <table border="1"> 167 * <caption>TilePane Constraint Table</caption> 168 * <tr><th scope="col">Constraint</th><th scope="col">Type</th><th scope="col">Description</th></tr> 169 * <tr><th scope="row">alignment</th><td>javafx.geometry.Pos</td><td>The alignment of the child within its tile.</td></tr> 170 * <tr><th scope="row">margin</th><td>javafx.geometry.Insets</td><td>Margin space around the outside of the child.</td></tr> 171 * </table> 172 * <p> 173 * Example: 174 * <pre>{@code 175 * TilePane tilepane = new TilePane(); 176 * for (int i = 0; i < 20; i++) { 177 * Label title = new Label(imageTitle[i]): 178 * Imageview imageview = new ImageView(new Image(imageName[i])); 179 * TilePane.setAlignment(label, Pos.BOTTOM_RIGHT); 180 * tilepane.getChildren().addAll(title, imageview); 181 * } 182 * }</pre> 183 * @since JavaFX 2.0 184 */ 185 public class TilePane extends Pane { 186 187 /******************************************************************** 188 * BEGIN static methods 189 ********************************************************************/ 190 191 private static final String MARGIN_CONSTRAINT = "tilepane-margin"; 192 private static final String ALIGNMENT_CONSTRAINT = "tilepane-alignment"; 193 194 /** 195 * Sets the alignment for the child when contained by a tilepane. 196 * If set, will override the tilepane's default alignment for children 197 * within their 'tiles'. 198 * Setting the value to null will remove the constraint. 199 * @param node the child node of a tilepane 200 * @param value the alignment position for the child 201 */ 202 public static void setAlignment(Node node, Pos value) { 203 setConstraint(node, ALIGNMENT_CONSTRAINT, value); 204 } 205 206 /** 207 * Returns the child's alignment constraint if set. 208 * @param node the child node of a tilepane 209 * @return the alignment position for the child or null if no alignment was set 210 */ 211 public static Pos getAlignment(Node node) { 212 return (Pos)getConstraint(node, ALIGNMENT_CONSTRAINT); 213 } 214 215 /** 216 * Sets the margin for the child when contained by a tilepane. 217 * If set, the tilepane will layout the child with the margin space around it. 218 * Setting the value to null will remove the constraint. 219 * @param node the child node of a tilepane 220 * @param value the margin of space around the child 221 */ 222 public static void setMargin(Node node, Insets value) { 223 setConstraint(node, MARGIN_CONSTRAINT, value); 224 } 225 226 /** 227 * Returns the child's margin constraint if set. 228 * @param node the child node of a tilepane 229 * @return the margin for the child or null if no margin was set 230 */ 231 public static Insets getMargin(Node node) { 232 return (Insets)getConstraint(node, MARGIN_CONSTRAINT); 233 } 234 235 private static final Callback<Node, Insets> marginAccessor = n -> getMargin(n); 236 237 /** 238 * Removes all tilepane constraints from the child node. 239 * @param child the child node 240 */ 241 public static void clearConstraints(Node child) { 242 setAlignment(child, null); 243 setMargin(child, null); 244 } 245 246 /******************************************************************** 247 * END static methods 248 ********************************************************************/ 249 250 private double _tileWidth = -1; 251 private double _tileHeight = -1; 252 253 /** 254 * Creates a horizontal TilePane layout with prefColumn = 5 and hgap/vgap = 0. 255 */ 256 public TilePane() { 257 super(); 258 } 259 260 /** 261 * Creates a TilePane layout with the specified orientation, 262 * prefColumn/prefRows = 5 and hgap/vgap = 0. 263 * @param orientation the direction the tiles should flow & wrap 264 */ 265 public TilePane(Orientation orientation) { 266 super(); 267 setOrientation(orientation); 268 } 269 270 /** 271 * Creates a horizontal TilePane layout with prefColumn = 5 and the specified 272 * hgap/vgap. 273 * @param hgap the amount of horizontal space between each tile 274 * @param vgap the amount of vertical space between each tile 275 */ 276 public TilePane(double hgap, double vgap) { 277 super(); 278 setHgap(hgap); 279 setVgap(vgap); 280 } 281 282 /** 283 * Creates a TilePane layout with the specified orientation, hgap/vgap, 284 * and prefRows/prefColumns = 5. 285 * @param orientation the direction the tiles should flow & wrap 286 * @param hgap the amount of horizontal space between each tile 287 * @param vgap the amount of vertical space between each tile 288 */ 289 public TilePane(Orientation orientation, double hgap, double vgap) { 290 this(); 291 setOrientation(orientation); 292 setHgap(hgap); 293 setVgap(vgap); 294 } 295 296 /** 297 * Creates a horizontal TilePane layout with prefColumn = 5 and hgap/vgap = 0. 298 * @param children The initial set of children for this pane. 299 * @since JavaFX 8.0 300 */ 301 public TilePane(Node... children) { 302 super(); 303 getChildren().addAll(children); 304 } 305 306 /** 307 * Creates a TilePane layout with the specified orientation, 308 * prefColumn/prefRows = 5 and hgap/vgap = 0. 309 * @param orientation the direction the tiles should flow & wrap 310 * @param children The initial set of children for this pane. 311 * @since JavaFX 8.0 312 */ 313 public TilePane(Orientation orientation, Node... children) { 314 super(); 315 setOrientation(orientation); 316 getChildren().addAll(children); 317 } 318 319 /** 320 * Creates a horizontal TilePane layout with prefColumn = 5 and the specified 321 * hgap/vgap. 322 * @param hgap the amount of horizontal space between each tile 323 * @param vgap the amount of vertical space between each tile 324 * @param children The initial set of children for this pane. 325 * @since JavaFX 8.0 326 */ 327 public TilePane(double hgap, double vgap, Node... children) { 328 super(); 329 setHgap(hgap); 330 setVgap(vgap); 331 getChildren().addAll(children); 332 } 333 334 /** 335 * Creates a TilePane layout with the specified orientation, hgap/vgap, 336 * and prefRows/prefColumns = 5. 337 * @param orientation the direction the tiles should flow & wrap 338 * @param hgap the amount of horizontal space between each tile 339 * @param vgap the amount of vertical space between each tile 340 * @param children The initial set of children for this pane. 341 * @since JavaFX 8.0 342 */ 343 public TilePane(Orientation orientation, double hgap, double vgap, Node... children) { 344 this(); 345 setOrientation(orientation); 346 setHgap(hgap); 347 setVgap(vgap); 348 getChildren().addAll(children); 349 } 350 351 /** 352 * The orientation of this tilepane. 353 * A horizontal tilepane lays out children in tiles, left to right, wrapping 354 * tiles at the tilepane's width boundary. A vertical tilepane lays out 355 * children in tiles, top to bottom, wrapping at the tilepane's height. 356 * The default is horizontal. 357 * @return the orientation of this tilepane 358 */ 359 public final ObjectProperty<Orientation> orientationProperty() { 360 if (orientation == null) { 361 orientation = new StyleableObjectProperty(HORIZONTAL) { 362 @Override 363 public void invalidated() { 364 requestLayout(); 365 } 366 367 @Override 368 public CssMetaData<TilePane, Orientation> getCssMetaData() { 369 return StyleableProperties.ORIENTATION; 370 } 371 372 @Override 373 public Object getBean() { 374 return TilePane.this; 375 } 376 377 @Override 378 public String getName() { 379 return "orientation"; 380 } 381 }; 382 } 383 return orientation; 384 } 385 386 private ObjectProperty<Orientation> orientation; 387 public final void setOrientation(Orientation value) { orientationProperty().set(value); } 388 public final Orientation getOrientation() { return orientation == null ? HORIZONTAL : orientation.get(); } 389 390 391 /** 392 * The preferred number of rows for a vertical tilepane. 393 * This value is used only to compute the preferred size of the tilepane 394 * and may not reflect the actual number of rows, which may change 395 * if the tilepane is resized to something other than its preferred height. 396 * This property is ignored for a horizontal tilepane. 397 * <p> 398 * It is recommended that the application initialize this value for a 399 * vertical tilepane. 400 * @return the preferred number of rows for a vertical tilepane 401 */ 402 public final IntegerProperty prefRowsProperty() { 403 if (prefRows == null) { 404 prefRows = new StyleableIntegerProperty(5) { 405 @Override 406 public void invalidated() { 407 requestLayout(); 408 } 409 410 @Override 411 public CssMetaData<TilePane, Number> getCssMetaData() { 412 return StyleableProperties.PREF_ROWS; 413 } 414 415 @Override 416 public Object getBean() { 417 return TilePane.this; 418 } 419 420 @Override 421 public String getName() { 422 return "prefRows"; 423 } 424 }; 425 } 426 return prefRows; 427 } 428 429 private IntegerProperty prefRows; 430 public final void setPrefRows(int value) { prefRowsProperty().set(value); } 431 public final int getPrefRows() { return prefRows == null ? 5 : prefRows.get(); } 432 433 /** 434 * The preferred number of columns for a horizontal tilepane. 435 * This value is used only to compute the preferred size of the tilepane 436 * and may not reflect the actual number of rows, which may change if the 437 * tilepane is resized to something other than its preferred height. 438 * This property is ignored for a vertical tilepane. 439 * <p> 440 * It is recommended that the application initialize this value for a 441 * horizontal tilepane. 442 * @return the preferred number of columns for a horizontal tilepane 443 */ 444 public final IntegerProperty prefColumnsProperty() { 445 if (prefColumns == null) { 446 prefColumns = new StyleableIntegerProperty(5) { 447 @Override 448 public void invalidated() { 449 requestLayout(); 450 } 451 452 @Override 453 public CssMetaData<TilePane, Number> getCssMetaData() { 454 return StyleableProperties.PREF_COLUMNS; 455 } 456 457 @Override 458 public Object getBean() { 459 return TilePane.this; 460 } 461 462 @Override 463 public String getName() { 464 return "prefColumns"; 465 } 466 }; 467 } 468 return prefColumns; 469 } 470 471 private IntegerProperty prefColumns; 472 public final void setPrefColumns(int value) { prefColumnsProperty().set(value); } 473 public final int getPrefColumns() { return prefColumns == null ? 5 : prefColumns.get(); } 474 475 /** 476 * The preferred width of each tile. 477 * If equal to USE_COMPUTED_SIZE (the default) the tile width wlll be 478 * automatically recomputed by the tilepane when the preferred size of children 479 * changes to accommodate the widest child. If the application sets this property 480 * to value greater than 0, then tiles will be set to that width and the tilepane 481 * will attempt to resize children to fit within that width (if they are resizable and 482 * their min-max width range allows it). 483 * @return the preferred width of each tile 484 */ 485 public final DoubleProperty prefTileWidthProperty() { 486 if (prefTileWidth == null) { 487 prefTileWidth = new StyleableDoubleProperty(USE_COMPUTED_SIZE) { 488 @Override 489 public void invalidated() { 490 requestLayout(); 491 } 492 493 @Override 494 public CssMetaData<TilePane, Number> getCssMetaData() { 495 return StyleableProperties.PREF_TILE_WIDTH; 496 } 497 498 @Override 499 public Object getBean() { 500 return TilePane.this; 501 } 502 503 @Override 504 public String getName() { 505 return "prefTileWidth"; 506 } 507 }; 508 } 509 return prefTileWidth; 510 } 511 512 private DoubleProperty prefTileWidth; 513 public final void setPrefTileWidth(double value) { prefTileWidthProperty().set(value); } 514 public final double getPrefTileWidth() { return prefTileWidth == null ? USE_COMPUTED_SIZE : prefTileWidth.get(); } 515 516 /** 517 * The preferred height of each tile. 518 * If equal to USE_COMPUTED_SIZE (the default) the tile height wlll be 519 * automatically recomputed by the tilepane when the preferred size of children 520 * changes to accommodate the tallest child. If the application sets this property 521 * to value greater than 0, then tiles will be set to that height and the tilepane 522 * will attempt to resize children to fit within that height (if they are resizable and 523 * their min-max height range allows it). 524 * @return the preferred height of each tile 525 */ 526 public final DoubleProperty prefTileHeightProperty() { 527 if (prefTileHeight == null) { 528 prefTileHeight = new StyleableDoubleProperty(USE_COMPUTED_SIZE) { 529 @Override 530 public void invalidated() { 531 requestLayout(); 532 } 533 534 @Override 535 public CssMetaData<TilePane, Number> getCssMetaData() { 536 return StyleableProperties.PREF_TILE_HEIGHT; 537 } 538 539 @Override 540 public Object getBean() { 541 return TilePane.this; 542 } 543 544 @Override 545 public String getName() { 546 return "prefTileHeight"; 547 } 548 }; 549 } 550 return prefTileHeight; 551 } 552 553 private DoubleProperty prefTileHeight; 554 public final void setPrefTileHeight(double value) { prefTileHeightProperty().set(value); } 555 public final double getPrefTileHeight() { return prefTileHeight == null ? USE_COMPUTED_SIZE : prefTileHeight.get(); } 556 557 /** 558 * The actual width of each tile. This property is read-only. 559 * @return the actual width of each tile 560 */ 561 public final ReadOnlyDoubleProperty tileWidthProperty() { 562 if (tileWidth == null) { 563 tileWidth = new TileSizeProperty("tileWidth", _tileWidth) { 564 565 @Override 566 public double compute() { 567 return computeTileWidth(); 568 } 569 570 }; 571 } 572 return tileWidth; 573 } 574 private TileSizeProperty tileWidth; 575 private void invalidateTileWidth() { 576 if (tileWidth != null) { 577 tileWidth.invalidate(); 578 } else { 579 _tileWidth = -1; 580 } 581 } 582 583 public final double getTileWidth() { 584 if (tileWidth != null) { 585 return tileWidth.get(); 586 } 587 if (_tileWidth == -1) { 588 _tileWidth = computeTileWidth(); 589 } 590 return _tileWidth; 591 } 592 593 /** 594 * The actual height of each tile. This property is read-only. 595 * @return the actual height of each tile 596 */ 597 public final ReadOnlyDoubleProperty tileHeightProperty() { 598 if (tileHeight == null) { 599 tileHeight = new TileSizeProperty("tileHeight", _tileHeight) { 600 601 @Override 602 public double compute() { 603 return computeTileHeight(); 604 } 605 606 }; 607 } 608 return tileHeight; 609 } 610 private TileSizeProperty tileHeight; 611 private void invalidateTileHeight() { 612 if (tileHeight != null) { 613 tileHeight.invalidate(); 614 } else { 615 _tileHeight = -1; 616 } 617 } 618 619 public final double getTileHeight() { 620 if (tileHeight != null) { 621 return tileHeight.get(); 622 } 623 if (_tileHeight == -1) { 624 _tileHeight = computeTileHeight(); 625 } 626 return _tileHeight; 627 } 628 629 /** 630 * The amount of horizontal space between each tile in a row. 631 * @return the amount of horizontal space between each tile in a row 632 */ 633 public final DoubleProperty hgapProperty() { 634 if (hgap == null) { 635 hgap = new StyleableDoubleProperty() { 636 @Override 637 public void invalidated() { 638 requestLayout(); 639 } 640 641 @Override 642 public CssMetaData<TilePane, Number> getCssMetaData() { 643 return StyleableProperties.HGAP; 644 } 645 646 @Override 647 public Object getBean() { 648 return TilePane.this; 649 } 650 651 @Override 652 public String getName() { 653 return "hgap"; 654 } 655 }; 656 } 657 return hgap; 658 } 659 660 private DoubleProperty hgap; 661 public final void setHgap(double value) { hgapProperty().set(value); } 662 public final double getHgap() { return hgap == null ? 0 : hgap.get(); } 663 664 /** 665 * The amount of vertical space between each tile in a column. 666 * @return the amount of vertical space between each tile in a column 667 */ 668 public final DoubleProperty vgapProperty() { 669 if (vgap == null) { 670 vgap = new StyleableDoubleProperty() { 671 @Override 672 public void invalidated() { 673 requestLayout(); 674 } 675 676 @Override 677 public CssMetaData<TilePane, Number> getCssMetaData() { 678 return StyleableProperties.VGAP; 679 } 680 681 @Override 682 public Object getBean() { 683 return TilePane.this; 684 } 685 686 @Override 687 public String getName() { 688 return "vgap"; 689 } 690 }; 691 } 692 return vgap; 693 } 694 695 private DoubleProperty vgap; 696 public final void setVgap(double value) { vgapProperty().set(value); } 697 public final double getVgap() { return vgap == null ? 0 : vgap.get(); } 698 699 /** 700 * The overall alignment of the tilepane's content within its width and height. 701 * <p>For a horizontal tilepane, each row will be aligned within the tilepane's width 702 * using the alignment's hpos value, and the rows will be aligned within the 703 * tilepane's height using the alignment's vpos value. 704 * <p>For a vertical tilepane, each column will be aligned within the tilepane's height 705 * using the alignment's vpos value, and the columns will be aligned within the 706 * tilepane's width using the alignment's hpos value. 707 * 708 * @return the overall alignment of the tilepane's content within its width 709 * and height 710 */ 711 public final ObjectProperty<Pos> alignmentProperty() { 712 if (alignment == null) { 713 alignment = new StyleableObjectProperty<Pos>(Pos.TOP_LEFT) { 714 @Override 715 public void invalidated() { 716 requestLayout(); 717 } 718 719 @Override 720 public CssMetaData<TilePane, Pos> getCssMetaData() { 721 return StyleableProperties.ALIGNMENT; 722 } 723 724 @Override 725 public Object getBean() { 726 return TilePane.this; 727 } 728 729 @Override 730 public String getName() { 731 return "alignment"; 732 } 733 }; 734 } 735 return alignment; 736 } 737 738 private ObjectProperty<Pos> alignment; 739 public final void setAlignment(Pos value) { alignmentProperty().set(value); } 740 public final Pos getAlignment() { return alignment == null ? Pos.TOP_LEFT : alignment.get(); } 741 private Pos getAlignmentInternal() { 742 Pos localPos = getAlignment(); 743 return localPos == null ? Pos.TOP_LEFT : localPos; 744 } 745 746 /** 747 * The default alignment of each child within its tile. 748 * This may be overridden on individual children by setting the child's 749 * alignment constraint. 750 * @return the default alignment of each child within its tile 751 */ 752 public final ObjectProperty<Pos> tileAlignmentProperty() { 753 if (tileAlignment == null) { 754 tileAlignment = new StyleableObjectProperty<Pos>(Pos.CENTER) { 755 @Override 756 public void invalidated() { 757 requestLayout(); 758 } 759 760 @Override 761 public CssMetaData<TilePane, Pos> getCssMetaData() { 762 return StyleableProperties.TILE_ALIGNMENT; 763 } 764 765 @Override 766 public Object getBean() { 767 return TilePane.this; 768 } 769 770 @Override 771 public String getName() { 772 return "tileAlignment"; 773 } 774 }; 775 } 776 return tileAlignment; 777 } 778 779 private ObjectProperty<Pos> tileAlignment; 780 public final void setTileAlignment(Pos value) { tileAlignmentProperty().set(value); } 781 public final Pos getTileAlignment() { return tileAlignment == null ? Pos.CENTER : tileAlignment.get(); } 782 private Pos getTileAlignmentInternal() { 783 Pos localPos = getTileAlignment(); 784 return localPos == null ? Pos.CENTER : localPos; 785 } 786 787 @Override public Orientation getContentBias() { 788 return getOrientation(); 789 } 790 791 @Override public void requestLayout() { 792 invalidateTileWidth(); 793 invalidateTileHeight(); 794 super.requestLayout(); 795 } 796 797 @Override protected double computeMinWidth(double height) { 798 if (getContentBias() == Orientation.HORIZONTAL) { 799 return getInsets().getLeft() + getTileWidth() + getInsets().getRight(); 800 } 801 return computePrefWidth(height); 802 } 803 804 @Override protected double computeMinHeight(double width) { 805 if (getContentBias() == Orientation.VERTICAL) { 806 return getInsets().getTop() + getTileHeight() + getInsets().getBottom(); 807 } 808 return computePrefHeight(width); 809 } 810 811 @Override protected double computePrefWidth(double forHeight) { 812 List<Node> managed = getManagedChildren(); 813 final Insets insets = getInsets(); 814 int prefCols = 0; 815 if (forHeight != -1) { 816 // first compute number of rows that will fit in given height and 817 // compute pref columns from that 818 int prefRows = computeRows(forHeight - snapSpaceY(insets.getTop()) - snapSpaceY(insets.getBottom()), getTileHeight()); 819 prefCols = computeOther(managed.size(), prefRows); 820 } else { 821 prefCols = getOrientation() == HORIZONTAL? getPrefColumns() : computeOther(managed.size(), getPrefRows()); 822 } 823 return snapSpaceX(insets.getLeft()) + 824 computeContentWidth(prefCols, getTileWidth()) + 825 snapSpaceX(insets.getRight()); 826 } 827 828 @Override protected double computePrefHeight(double forWidth) { 829 List<Node> managed = getManagedChildren(); 830 final Insets insets = getInsets(); 831 int prefRows = 0; 832 if (forWidth != -1) { 833 // first compute number of columns that will fit in given width and 834 // compute pref rows from that 835 int prefCols = computeColumns(forWidth - snapSpaceX(insets.getLeft()) - snapSpaceX(insets.getRight()), getTileWidth()); 836 prefRows = computeOther(managed.size(), prefCols); 837 } else { 838 prefRows = getOrientation() == HORIZONTAL? computeOther(managed.size(), getPrefColumns()) : getPrefRows(); 839 } 840 return snapSpaceY(insets.getTop()) + 841 computeContentHeight(prefRows, getTileHeight()) + 842 snapSpaceY(insets.getBottom()); 843 } 844 845 private double computeTileWidth() { 846 List<Node> managed = getManagedChildren(); 847 double preftilewidth = getPrefTileWidth(); 848 if (preftilewidth == USE_COMPUTED_SIZE) { 849 double h = -1; 850 boolean vertBias = false; 851 for (int i = 0, size = managed.size(); i < size; i++) { 852 Node child = managed.get(i); 853 if (child.getContentBias() == VERTICAL) { 854 vertBias = true; 855 break; 856 } 857 } 858 if (vertBias) { 859 // widest may depend on height of tile 860 h = computeMaxPrefAreaHeight(managed, marginAccessor, -1, getTileAlignmentInternal().getVpos()); 861 } 862 return snapSizeX(computeMaxPrefAreaWidth(managed, marginAccessor, h, true)); 863 } 864 return snapSizeX(preftilewidth); 865 } 866 867 private double computeTileHeight() { 868 List<Node> managed = getManagedChildren(); 869 double preftileheight = getPrefTileHeight(); 870 if (preftileheight == USE_COMPUTED_SIZE) { 871 double w = -1; 872 boolean horizBias = false; 873 for (int i = 0, size = managed.size(); i < size; i++) { 874 Node child = managed.get(i); 875 if (child.getContentBias() == Orientation.HORIZONTAL) { 876 horizBias = true; 877 break; 878 } 879 } 880 if (horizBias) { 881 // tallest may depend on width of tile 882 w = computeMaxPrefAreaWidth(managed, marginAccessor); 883 } 884 return snapSizeY(computeMaxPrefAreaHeight(managed, marginAccessor, w, getTileAlignmentInternal().getVpos())); 885 } 886 return snapSizeY(preftileheight); 887 } 888 889 private int computeOther(int numNodes, int numCells) { 890 double other = (double)numNodes/(double)Math.max(1, numCells); 891 return (int)Math.ceil(other); 892 } 893 894 private int computeColumns(double width, double tilewidth) { 895 double snappedHgap = snapSpaceX(getHgap()); 896 return Math.max(1,(int)((width + snappedHgap) / (tilewidth + snappedHgap))); 897 } 898 899 private int computeRows(double height, double tileheight) { 900 double snappedVgap = snapSpaceY(getVgap()); 901 return Math.max(1, (int)((height + snappedVgap) / (tileheight + snappedVgap))); 902 } 903 904 private double computeContentWidth(int columns, double tilewidth) { 905 if (columns == 0) return 0; 906 return columns * tilewidth + (columns - 1) * snapSpaceX(getHgap()); 907 } 908 909 private double computeContentHeight(int rows, double tileheight) { 910 if (rows == 0) return 0; 911 return rows * tileheight + (rows - 1) * snapSpaceY(getVgap()); 912 } 913 914 @Override protected void layoutChildren() { 915 List<Node> managed = getManagedChildren(); 916 HPos hpos = getAlignmentInternal().getHpos(); 917 VPos vpos = getAlignmentInternal().getVpos(); 918 double width = getWidth(); 919 double height = getHeight(); 920 double top = snapSpaceY(getInsets().getTop()); 921 double left = snapSpaceX(getInsets().getLeft()); 922 double bottom = snapSpaceY(getInsets().getBottom()); 923 double right = snapSpaceX(getInsets().getRight()); 924 double vgap = snapSpaceY(getVgap()); 925 double hgap = snapSpaceX(getHgap()); 926 double insideWidth = width - left - right; 927 double insideHeight = height - top - bottom; 928 929 double tileWidth = getTileWidth() > insideWidth ? insideWidth : getTileWidth(); 930 double tileHeight = getTileHeight() > insideHeight ? insideHeight : getTileHeight(); 931 932 int lastRowRemainder = 0; 933 int lastColumnRemainder = 0; 934 if (getOrientation() == HORIZONTAL) { 935 actualColumns = computeColumns(insideWidth, tileWidth); 936 actualRows = computeOther(managed.size(), actualColumns); 937 // remainder will be 0 if last row is filled 938 lastRowRemainder = hpos != HPos.LEFT? 939 actualColumns - (actualColumns*actualRows - managed.size()) : 0; 940 } else { 941 // vertical 942 actualRows = computeRows(insideHeight, tileHeight); 943 actualColumns = computeOther(managed.size(), actualRows); 944 // remainder will be 0 if last column is filled 945 lastColumnRemainder = vpos != VPos.TOP? 946 actualRows - (actualColumns*actualRows - managed.size()) : 0; 947 } 948 double rowX = left + computeXOffset(insideWidth, 949 computeContentWidth(actualColumns, tileWidth), 950 hpos); 951 double columnY = top + computeYOffset(insideHeight, 952 computeContentHeight(actualRows, tileHeight), 953 vpos); 954 955 double lastRowX = lastRowRemainder > 0? 956 left + computeXOffset(insideWidth, 957 computeContentWidth(lastRowRemainder, tileWidth), 958 hpos) : rowX; 959 double lastColumnY = lastColumnRemainder > 0? 960 top + computeYOffset(insideHeight, 961 computeContentHeight(lastColumnRemainder, tileHeight), 962 vpos) : columnY; 963 double baselineOffset = getTileAlignmentInternal().getVpos() == VPos.BASELINE ? 964 getAreaBaselineOffset(managed, marginAccessor, i -> tileWidth, tileHeight, false) : -1; 965 966 int r = 0; 967 int c = 0; 968 for (int i = 0, size = managed.size(); i < size; i++) { 969 Node child = managed.get(i); 970 double xoffset = r == (actualRows - 1)? lastRowX : rowX; 971 double yoffset = c == (actualColumns - 1)? lastColumnY : columnY; 972 973 double tileX = xoffset + (c * (tileWidth + hgap)); 974 double tileY = yoffset + (r * (tileHeight + vgap)); 975 976 Pos childAlignment = getAlignment(child); 977 978 layoutInArea(child, tileX, tileY, tileWidth, tileHeight, baselineOffset, 979 getMargin(child), 980 childAlignment != null? childAlignment.getHpos() : getTileAlignmentInternal().getHpos(), 981 childAlignment != null? childAlignment.getVpos() : getTileAlignmentInternal().getVpos()); 982 983 if (getOrientation() == HORIZONTAL) { 984 if (++c == actualColumns) { 985 c = 0; 986 r++; 987 } 988 } else { 989 // vertical 990 if (++r == actualRows) { 991 r = 0; 992 c++; 993 } 994 } 995 } 996 } 997 998 private int actualRows = 0; 999 private int actualColumns = 0; 1000 1001 /*************************************************************************** 1002 * * 1003 * Stylesheet Handling * 1004 * * 1005 **************************************************************************/ 1006 1007 1008 /* 1009 * Super-lazy instantiation pattern from Bill Pugh. 1010 */ 1011 private static class StyleableProperties { 1012 1013 private static final CssMetaData<TilePane,Pos> ALIGNMENT = 1014 new CssMetaData<TilePane,Pos>("-fx-alignment", 1015 new EnumConverter<Pos>(Pos.class), 1016 Pos.TOP_LEFT) { 1017 1018 @Override 1019 public boolean isSettable(TilePane node) { 1020 return node.alignment == null || !node.alignment.isBound(); 1021 } 1022 1023 @Override 1024 public StyleableProperty<Pos> getStyleableProperty(TilePane node) { 1025 return (StyleableProperty<Pos>)node.alignmentProperty(); 1026 } 1027 }; 1028 1029 private static final CssMetaData<TilePane,Number> PREF_COLUMNS = 1030 new CssMetaData<TilePane,Number>("-fx-pref-columns", 1031 SizeConverter.getInstance(), 5.0) { 1032 1033 @Override 1034 public boolean isSettable(TilePane node) { 1035 return node.prefColumns == null || 1036 !node.prefColumns.isBound(); 1037 } 1038 1039 @Override 1040 public StyleableProperty<Number> getStyleableProperty(TilePane node) { 1041 return (StyleableProperty<Number>)node.prefColumnsProperty(); 1042 } 1043 }; 1044 1045 private static final CssMetaData<TilePane,Number> HGAP = 1046 new CssMetaData<TilePane,Number>("-fx-hgap", 1047 SizeConverter.getInstance(), 0.0) { 1048 1049 @Override 1050 public boolean isSettable(TilePane node) { 1051 return node.hgap == null || 1052 !node.hgap.isBound(); 1053 } 1054 1055 @Override 1056 public StyleableProperty<Number> getStyleableProperty(TilePane node) { 1057 return (StyleableProperty<Number>)node.hgapProperty(); 1058 } 1059 }; 1060 1061 private static final CssMetaData<TilePane,Number> PREF_ROWS = 1062 new CssMetaData<TilePane,Number>("-fx-pref-rows", 1063 SizeConverter.getInstance(), 5.0) { 1064 1065 @Override 1066 public boolean isSettable(TilePane node) { 1067 return node.prefRows == null || 1068 !node.prefRows.isBound(); 1069 } 1070 1071 @Override 1072 public StyleableProperty<Number> getStyleableProperty(TilePane node) { 1073 return (StyleableProperty<Number>)node.prefRowsProperty(); 1074 } 1075 }; 1076 1077 private static final CssMetaData<TilePane,Pos> TILE_ALIGNMENT = 1078 new CssMetaData<TilePane,Pos>("-fx-tile-alignment", 1079 new EnumConverter<Pos>(Pos.class), 1080 Pos.CENTER) { 1081 1082 @Override 1083 public boolean isSettable(TilePane node) { 1084 return node.tileAlignment == null || 1085 !node.tileAlignment.isBound(); 1086 } 1087 1088 @Override 1089 public StyleableProperty<Pos> getStyleableProperty(TilePane node) { 1090 return (StyleableProperty<Pos>)node.tileAlignmentProperty(); 1091 } 1092 }; 1093 1094 private static final CssMetaData<TilePane,Number> PREF_TILE_WIDTH = 1095 new CssMetaData<TilePane,Number>("-fx-pref-tile-width", 1096 SizeConverter.getInstance(), USE_COMPUTED_SIZE) { 1097 1098 @Override 1099 public boolean isSettable(TilePane node) { 1100 return node.prefTileWidth == null || 1101 !node.prefTileWidth.isBound(); 1102 } 1103 1104 @Override 1105 public StyleableProperty<Number> getStyleableProperty(TilePane node) { 1106 return (StyleableProperty<Number>)node.prefTileWidthProperty(); 1107 } 1108 }; 1109 1110 private static final CssMetaData<TilePane,Number> PREF_TILE_HEIGHT = 1111 new CssMetaData<TilePane,Number>("-fx-pref-tile-height", 1112 SizeConverter.getInstance(), USE_COMPUTED_SIZE) { 1113 1114 @Override 1115 public boolean isSettable(TilePane node) { 1116 return node.prefTileHeight == null || 1117 !node.prefTileHeight.isBound(); 1118 } 1119 1120 @Override 1121 public StyleableProperty<Number> getStyleableProperty(TilePane node) { 1122 return (StyleableProperty<Number>)node.prefTileHeightProperty(); 1123 } 1124 }; 1125 1126 private static final CssMetaData<TilePane,Orientation> ORIENTATION = 1127 new CssMetaData<TilePane,Orientation>("-fx-orientation", 1128 new EnumConverter<Orientation>(Orientation.class), 1129 Orientation.HORIZONTAL) { 1130 1131 @Override 1132 public Orientation getInitialValue(TilePane node) { 1133 // A vertical TilePane should remain vertical 1134 return node.getOrientation(); 1135 } 1136 1137 @Override 1138 public boolean isSettable(TilePane node) { 1139 return node.orientation == null || 1140 !node.orientation.isBound(); 1141 } 1142 1143 @Override 1144 public StyleableProperty<Orientation> getStyleableProperty(TilePane node) { 1145 return (StyleableProperty<Orientation>)node.orientationProperty(); 1146 } 1147 }; 1148 1149 private static final CssMetaData<TilePane,Number> VGAP = 1150 new CssMetaData<TilePane,Number>("-fx-vgap", 1151 SizeConverter.getInstance(), 0.0) { 1152 1153 @Override 1154 public boolean isSettable(TilePane node) { 1155 return node.vgap == null || 1156 !node.vgap.isBound(); 1157 } 1158 1159 @Override 1160 public StyleableProperty<Number> getStyleableProperty(TilePane node) { 1161 return (StyleableProperty<Number>)node.vgapProperty(); 1162 } 1163 }; 1164 1165 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 1166 static { 1167 final List<CssMetaData<? extends Styleable, ?>> styleables = 1168 new ArrayList<CssMetaData<? extends Styleable, ?>>(Region.getClassCssMetaData()); 1169 styleables.add(ALIGNMENT); 1170 styleables.add(HGAP); 1171 styleables.add(ORIENTATION); 1172 styleables.add(PREF_COLUMNS); 1173 styleables.add(PREF_ROWS); 1174 styleables.add(PREF_TILE_WIDTH); 1175 styleables.add(PREF_TILE_HEIGHT); 1176 styleables.add(TILE_ALIGNMENT); 1177 styleables.add(VGAP); 1178 STYLEABLES = Collections.unmodifiableList(styleables); 1179 } 1180 } 1181 1182 /** 1183 * @return The CssMetaData associated with this class, which may include the 1184 * CssMetaData of its superclasses. 1185 * @since JavaFX 8.0 1186 */ 1187 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 1188 return StyleableProperties.STYLEABLES; 1189 } 1190 1191 /** 1192 * {@inheritDoc} 1193 * 1194 * @since JavaFX 8.0 1195 */ 1196 1197 1198 @Override 1199 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 1200 return getClassCssMetaData(); 1201 } 1202 1203 private abstract class TileSizeProperty extends ReadOnlyDoubleProperty { 1204 private final String name; 1205 private ExpressionHelper<Number> helper; 1206 private double value; 1207 private boolean valid; 1208 1209 TileSizeProperty(String name, double initSize) { 1210 this.name = name; 1211 this.value = initSize; 1212 this.valid = initSize != -1; 1213 } 1214 1215 1216 @Override 1217 public Object getBean() { 1218 return TilePane.this; 1219 } 1220 1221 @Override 1222 public String getName() { 1223 return name; 1224 } 1225 1226 @Override 1227 public void addListener(InvalidationListener listener) { 1228 helper = ExpressionHelper.addListener(helper, this, listener); 1229 } 1230 1231 @Override 1232 public void removeListener(InvalidationListener listener) { 1233 helper = ExpressionHelper.removeListener(helper, listener); 1234 } 1235 1236 @Override 1237 public void addListener(ChangeListener<? super Number> listener) { 1238 helper = ExpressionHelper.addListener(helper, this, listener); 1239 } 1240 1241 @Override 1242 public void removeListener(ChangeListener<? super Number> listener) { 1243 helper = ExpressionHelper.removeListener(helper, listener); 1244 } 1245 1246 @Override 1247 public double get() { 1248 if (!valid) { 1249 value = compute(); 1250 valid = true; 1251 } 1252 1253 return value; 1254 } 1255 1256 public void invalidate() { 1257 if (valid) { 1258 valid = false; 1259 ExpressionHelper.fireValueChangedEvent(helper); 1260 } 1261 } 1262 1263 public abstract double compute(); 1264 } 1265 1266 }