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