1 /* 2 * Copyright (c) 2011, 2014, 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.beans.property.ReadOnlyDoubleWrapper; 37 import javafx.css.CssMetaData; 38 import javafx.css.StyleableDoubleProperty; 39 import javafx.css.StyleableIntegerProperty; 40 import javafx.css.StyleableObjectProperty; 41 import javafx.css.StyleableProperty; 42 import javafx.geometry.HPos; 43 import javafx.geometry.Insets; 44 import javafx.geometry.Orientation; 45 import javafx.geometry.Pos; 46 import javafx.geometry.VPos; 47 import javafx.scene.Node; 48 import com.sun.javafx.css.converters.EnumConverter; 49 import com.sun.javafx.css.converters.SizeConverter; 50 import java.util.function.Function; 51 import javafx.beans.InvalidationListener; 52 import javafx.beans.value.ChangeListener; 53 import javafx.css.Styleable; 54 55 import static javafx.geometry.Orientation.*; 56 import javafx.util.Callback; 57 58 59 /** 60 * TilePane lays out its children in a grid of uniformly sized "tiles". 61 * <p> 62 * A horizontal tilepane (the default) will tile nodes in rows, wrapping at the 63 * tilepane's width. A vertical tilepane will tile nodes in columns, 64 * wrapping at the tilepane's height. 65 * <p> 66 * The size of each "tile" defaults to the size needed to encompass the largest 67 * preferred width and height of the tilepane's children and the tilepane 68 * will recompute the size of the tiles as needed to accommodate the largest preferred 69 * size of its children as it changes. The application may also control the size 70 * of the tiles directly by setting prefTileWidth/prefTileHeight 71 * properties to a value other than USE_COMPUTED_SIZE (the default). 72 * <p> 73 * Applications should initialize either <code>prefColumns</code> (for horizontal) 74 * or <code>prefRows</code> (for vertical) to establish the tilepane's preferred 75 * size (the arbitrary default is 5). Note that prefColumns/prefRows 76 * is used only for calculating the preferred size and may not reflect the actual 77 * number of rows or columns, which may change as the tilepane is resized and 78 * the tiles are wrapped at its actual boundaries. 79 * <p> 80 * The alignment property controls how the rows and columns are aligned 81 * within the bounds of the tilepane and defaults to Pos.TOP_LEFT. It is also possible 82 * to control the alignment of nodes within the individual tiles by setting 83 * {@link #tileAlignmentProperty() tileAlignment}, which defaults to Pos.CENTER. 84 * <p> 85 * A horizontal tilepane example: 86 * <pre><code> 87 * TilePane tile = new TilePane(); 88 * tile.setHgap(8); 89 * tile.setPrefColumns(4); 90 * for (int i = 0; i < 20; i++) { 91 * tile.getChildren().add(new ImageView(...)); 92 * } 93 * </code></pre> 94 * <p> 95 * A vertical TilePane example: 96 * <pre><code> 97 * TilePane tile = new TilePane(Orientation.VERTICAL); 98 * tile.setTileAlignment(Pos.CENTER_LEFT); 99 * tile.setPrefRows(10); 100 * for (int i = 0; i < 50; i++) { 101 * tile.getChildren().add(new ImageView(...)); 102 * } 103 * </code></pre> 104 * 105 * The TilePane will attempt to resize each child to fill its tile. 106 * If the child could not be sized to fill the tile (either because it was not 107 * resizable or its size limits prevented it) then it will be aligned within the 108 * tile using tileAlignment. 109 * 110 * <h4>Resizable Range</h4> 111 * 112 * A tilepane's parent will resize the tilepane within the tilepane's resizable range 113 * during layout. By default the tilepane computes this range based on its content 114 * as outlined in the tables below. 115 * <p> 116 * Horizontal: 117 * <table border="1"> 118 * <tr><td></td><th>width</th><th>height</th></tr> 119 * <tr><th>minimum</th> 120 * <td>left/right insets plus the tile width.</td> 121 * <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> 122 * <tr><th>preferred</th> 123 * <td>left/right insets plus prefColumns multiplied by the tile width.</td> 124 * <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> 125 * <tr><th>maximum</th> 126 * <td>Double.MAX_VALUE</td><td>Double.MAX_VALUE</td></tr> 127 * </table> 128 * <p> 129 * Vertical: 130 * <table border="1"> 131 * <tr><td></td><th>width</th><th>height</th></tr> 132 * <tr><th>minimum</th> 133 * <td>left/right insets plus width required to display all tiles when wrapped at a specified height with an hgap between each column.</td> 134 * <td>top/bottom insets plus the tile height.</td><tr> 135 * <tr><th>preferred</th> 136 * <td>left/right insets plus width required to display all tiles when wrapped at the specified height with an hgap between each column.</td> 137 * <td>top/bottom insets plus prefRows multiplied by the tile height.</td><tr> 138 * <tr><th>maximum</th> 139 * <td>Double.MAX_VALUE</td><td>Double.MAX_VALUE</td></tr> 140 * </table> 141 * <p> 142 * A tilepane's unbounded maximum width and height are an indication to the parent that 143 * it may be resized beyond its preferred size to fill whatever space is assigned to it. 144 * <p> 145 * TilePane provides properties for setting the size range directly. These 146 * properties default to the sentinel value Region.USE_COMPUTED_SIZE, however the 147 * application may set them to other values as needed: 148 * <pre><code> 149 * <b>tilepane.setMaxWidth(500);</b> 150 * </code></pre> 151 * Applications may restore the computed values by setting these properties back 152 * to Region.USE_COMPUTED_SIZE. 153 * <p> 154 * TilePane does not clip its content by default, so it is possible that childrens' 155 * bounds may extend outside the tiles (and possibly the tilepane bounds) if a 156 * child's pref size prevents it from being fit within its tile. Also, if the tilepane 157 * is resized smaller than its preferred size, it may not be able to fit all the 158 * tiles within its bounds and the content will extend outside. 159 * 160 * <h4>Optional Layout Constraints</h4> 161 * 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 * <table border="1"> 166 * <tr><th>Constraint</th><th>Type</th><th>Description</th></tr> 167 * <tr><td>alignment</td><td>javafx.geometry.Pos</td><td>The alignment of the child within its tile.</td></tr> 168 * <tr><td>margin</td><td>javafx.geometry.Insets</td><td>Margin space around the outside of the child.</td></tr> 169 * </table> 170 * <p> 171 * Example: 172 * <pre><code> 173 * TilePane tilepane = new TilePane(); 174 * for (int i = 0; i < 20; i++) { 175 * Label title = new Label(imageTitle[i]): 176 * Imageview imageview = new ImageView(new Image(imageName[i])); 177 * TilePane.setAlignment(label, Pos.BOTTOM_RIGHT); 178 * tilepane.getChildren().addAll(title, imageview); 179 * } 180 * </code></pre> 181 * @since JavaFX 2.0 182 */ 183 public class TilePane extends Pane { 184 185 /******************************************************************** 186 * BEGIN static methods 187 ********************************************************************/ 188 189 private static final String MARGIN_CONSTRAINT = "tilepane-margin"; 190 private static final String ALIGNMENT_CONSTRAINT = "tilepane-alignment"; 191 192 /** 193 * Sets the alignment for the child when contained by a tilepane. 194 * If set, will override the tilepane's default alignment for children 195 * within their 'tiles'. 196 * Setting the value to null will remove the constraint. 197 * @param node the child node of a tilepane 198 * @param value the alignment position for the child 199 */ 200 public static void setAlignment(Node node, Pos value) { 201 setConstraint(node, ALIGNMENT_CONSTRAINT, value); 202 } 203 204 /** 205 * Returns the child's alignment constraint if set. 206 * @param node the child node of a tilepane 207 * @return the alignment position for the child or null if no alignment was set 208 */ 209 public static Pos getAlignment(Node node) { 210 return (Pos)getConstraint(node, ALIGNMENT_CONSTRAINT); 211 } 212 213 /** 214 * Sets the margin for the child when contained by a tilepane. 215 * If set, the tilepane will layout the child with the margin space around it. 216 * Setting the value to null will remove the constraint. 217 * @param node the child node of a tilepane 218 * @param value the margin of space around the child 219 */ 220 public static void setMargin(Node node, Insets value) { 221 setConstraint(node, MARGIN_CONSTRAINT, value); 222 } 223 224 /** 225 * Returns the child's margin constraint if set. 226 * @param node the child node of a tilepane 227 * @return the margin for the child or null if no margin was set 228 */ 229 public static Insets getMargin(Node node) { 230 return (Insets)getConstraint(node, MARGIN_CONSTRAINT); 231 } 232 233 private static final Callback<Node, Insets> marginAccessor = n -> getMargin(n); 234 235 /** 236 * Removes all tilepane constraints from the child node. 237 * @param child the child node 238 */ 239 public static void clearConstraints(Node child) { 240 setAlignment(child, null); 241 setMargin(child, null); 242 } 243 244 /******************************************************************** 245 * END static methods 246 ********************************************************************/ 247 248 private double _tileWidth = -1; 249 private double _tileHeight = -1; 250 251 /** 252 * Creates a horizontal TilePane layout with prefColumn = 5 and hgap/vgap = 0. 253 */ 254 public TilePane() { 255 super(); 256 } 257 258 /** 259 * Creates a TilePane layout with the specified orientation, 260 * prefColumn/prefRows = 5 and hgap/vgap = 0. 261 * @param orientation the direction the tiles should flow & wrap 262 */ 263 public TilePane(Orientation orientation) { 264 super(); 265 setOrientation(orientation); 266 } 267 268 /** 269 * Creates a horizontal TilePane layout with prefColumn = 5 and the specified 270 * hgap/vgap. 271 * @param hgap the amount of horizontal space between each tile 272 * @param vgap the amount of vertical space between each tile 273 */ 274 public TilePane(double hgap, double vgap) { 275 super(); 276 setHgap(hgap); 277 setVgap(vgap); 278 } 279 280 /** 281 * Creates a TilePane layout with the specified orientation, hgap/vgap, 282 * and prefRows/prefColumns = 5. 283 * @param orientation the direction the tiles should flow & wrap 284 * @param hgap the amount of horizontal space between each tile 285 * @param vgap the amount of vertical space between each tile 286 */ 287 public TilePane(Orientation orientation, double hgap, double vgap) { 288 this(); 289 setOrientation(orientation); 290 setHgap(hgap); 291 setVgap(vgap); 292 } 293 294 /** 295 * Creates a horizontal TilePane layout with prefColumn = 5 and hgap/vgap = 0. 296 * @param children The initial set of children for this pane. 297 * @since JavaFX 8.0 298 */ 299 public TilePane(Node... children) { 300 super(); 301 getChildren().addAll(children); 302 } 303 304 /** 305 * Creates a TilePane layout with the specified orientation, 306 * prefColumn/prefRows = 5 and hgap/vgap = 0. 307 * @param orientation the direction the tiles should flow & wrap 308 * @param children The initial set of children for this pane. 309 * @since JavaFX 8.0 310 */ 311 public TilePane(Orientation orientation, Node... children) { 312 super(); 313 setOrientation(orientation); 314 getChildren().addAll(children); 315 } 316 317 /** 318 * Creates a horizontal TilePane layout with prefColumn = 5 and the specified 319 * hgap/vgap. 320 * @param hgap the amount of horizontal space between each tile 321 * @param vgap the amount of vertical space between each tile 322 * @param children The initial set of children for this pane. 323 * @since JavaFX 8.0 324 */ 325 public TilePane(double hgap, double vgap, Node... children) { 326 super(); 327 setHgap(hgap); 328 setVgap(vgap); 329 getChildren().addAll(children); 330 } 331 332 /** 333 * Creates a TilePane layout with the specified orientation, hgap/vgap, 334 * and prefRows/prefColumns = 5. 335 * @param orientation the direction the tiles should flow & wrap 336 * @param hgap the amount of horizontal space between each tile 337 * @param vgap the amount of vertical space between each tile 338 * @param children The initial set of children for this pane. 339 * @since JavaFX 8.0 340 */ 341 public TilePane(Orientation orientation, double hgap, double vgap, Node... children) { 342 this(); 343 setOrientation(orientation); 344 setHgap(hgap); 345 setVgap(vgap); 346 getChildren().addAll(children); 347 } 348 349 /** 350 * The orientation of this tilepane. 351 * A horizontal tilepane lays out children in tiles, left to right, wrapping 352 * tiles at the tilepane's width boundary. A vertical tilepane lays out 353 * children in tiles, top to bottom, wrapping at the tilepane's height. 354 * The default is horizontal. 355 */ 356 public final ObjectProperty<Orientation> orientationProperty() { 357 if (orientation == null) { 358 orientation = new StyleableObjectProperty(HORIZONTAL) { 359 @Override 360 public void invalidated() { 361 requestLayout(); 362 } 363 364 @Override 365 public CssMetaData<TilePane, Orientation> getCssMetaData() { 366 return StyleableProperties.ORIENTATION; 367 } 368 369 @Override 370 public Object getBean() { 371 return TilePane.this; 372 } 373 374 @Override 375 public String getName() { 376 return "orientation"; 377 } 378 }; 379 } 380 return orientation; 381 } 382 383 private ObjectProperty<Orientation> orientation; 384 public final void setOrientation(Orientation value) { orientationProperty().set(value); } 385 public final Orientation getOrientation() { return orientation == null ? HORIZONTAL : orientation.get(); } 386 387 388 /** 389 * The preferred number of rows for a vertical tilepane. 390 * This value is used only to compute the preferred size of the tilepane 391 * and may not reflect the actual number of rows, which may change 392 * if the tilepane is resized to something other than its preferred height. 393 * This property is ignored for a horizontal tilepane. 394 * <p> 395 * It is recommended that the application initialize this value for a 396 * vertical tilepane. 397 */ 398 public final IntegerProperty prefRowsProperty() { 399 if (prefRows == null) { 400 prefRows = new StyleableIntegerProperty(5) { 401 @Override 402 public void invalidated() { 403 requestLayout(); 404 } 405 406 @Override 407 public CssMetaData<TilePane, Number> getCssMetaData() { 408 return StyleableProperties.PREF_ROWS; 409 } 410 411 @Override 412 public Object getBean() { 413 return TilePane.this; 414 } 415 416 @Override 417 public String getName() { 418 return "prefRows"; 419 } 420 }; 421 } 422 return prefRows; 423 } 424 425 private IntegerProperty prefRows; 426 public final void setPrefRows(int value) { prefRowsProperty().set(value); } 427 public final int getPrefRows() { return prefRows == null ? 5 : prefRows.get(); } 428 429 /** 430 * The preferred number of columns for a horizontal tilepane. 431 * This value is used only to compute the preferred size of the tilepane 432 * and may not reflect the actual number of rows, which may change if the 433 * tilepane is resized to something other than its preferred height. 434 * This property is ignored for a vertical tilepane. 435 * <p> 436 * It is recommended that the application initialize this value for a 437 * horizontal tilepane. 438 */ 439 public final IntegerProperty prefColumnsProperty() { 440 if (prefColumns == null) { 441 prefColumns = new StyleableIntegerProperty(5) { 442 @Override 443 public void invalidated() { 444 requestLayout(); 445 } 446 447 @Override 448 public CssMetaData<TilePane, Number> getCssMetaData() { 449 return StyleableProperties.PREF_COLUMNS; 450 } 451 452 @Override 453 public Object getBean() { 454 return TilePane.this; 455 } 456 457 @Override 458 public String getName() { 459 return "prefColumns"; 460 } 461 }; 462 } 463 return prefColumns; 464 } 465 466 private IntegerProperty prefColumns; 467 public final void setPrefColumns(int value) { prefColumnsProperty().set(value); } 468 public final int getPrefColumns() { return prefColumns == null ? 5 : prefColumns.get(); } 469 470 /** 471 * The preferred width of each tile. 472 * If equal to USE_COMPUTED_SIZE (the default) the tile width wlll be 473 * automatically recomputed by the tilepane when the preferred size of children 474 * changes to accommodate the widest child. If the application sets this property 475 * to value greater than 0, then tiles will be set to that width and the tilepane 476 * will attempt to resize children to fit within that width (if they are resizable and 477 * their min-max width range allows it). 478 */ 479 public final DoubleProperty prefTileWidthProperty() { 480 if (prefTileWidth == null) { 481 prefTileWidth = new StyleableDoubleProperty(USE_COMPUTED_SIZE) { 482 @Override 483 public void invalidated() { 484 requestLayout(); 485 } 486 487 @Override 488 public CssMetaData<TilePane, Number> getCssMetaData() { 489 return StyleableProperties.PREF_TILE_WIDTH; 490 } 491 492 @Override 493 public Object getBean() { 494 return TilePane.this; 495 } 496 497 @Override 498 public String getName() { 499 return "prefTileWidth"; 500 } 501 }; 502 } 503 return prefTileWidth; 504 } 505 506 private DoubleProperty prefTileWidth; 507 public final void setPrefTileWidth(double value) { prefTileWidthProperty().set(value); } 508 public final double getPrefTileWidth() { return prefTileWidth == null ? USE_COMPUTED_SIZE : prefTileWidth.get(); } 509 510 /** 511 * The preferred height of each tile. 512 * If equal to USE_COMPUTED_SIZE (the default) the tile height wlll be 513 * automatically recomputed by the tilepane when the preferred size of children 514 * changes to accommodate the tallest child. If the application sets this property 515 * to value greater than 0, then tiles will be set to that height and the tilepane 516 * will attempt to resize children to fit within that height (if they are resizable and 517 * their min-max height range allows it). 518 */ 519 public final DoubleProperty prefTileHeightProperty() { 520 if (prefTileHeight == null) { 521 prefTileHeight = new StyleableDoubleProperty(USE_COMPUTED_SIZE) { 522 @Override 523 public void invalidated() { 524 requestLayout(); 525 } 526 527 @Override 528 public CssMetaData<TilePane, Number> getCssMetaData() { 529 return StyleableProperties.PREF_TILE_HEIGHT; 530 } 531 532 @Override 533 public Object getBean() { 534 return TilePane.this; 535 } 536 537 @Override 538 public String getName() { 539 return "prefTileHeight"; 540 } 541 }; 542 } 543 return prefTileHeight; 544 } 545 546 private DoubleProperty prefTileHeight; 547 public final void setPrefTileHeight(double value) { prefTileHeightProperty().set(value); } 548 public final double getPrefTileHeight() { return prefTileHeight == null ? USE_COMPUTED_SIZE : prefTileHeight.get(); } 549 550 /** 551 * The actual width of each tile. This property is read-only. 552 */ 553 public final ReadOnlyDoubleProperty tileWidthProperty() { 554 if (tileWidth == null) { 555 tileWidth = new TileSizeProperty("tileWidth", _tileWidth) { 556 557 @Override 558 public double compute() { 559 return computeTileWidth(); 560 } 561 562 }; 563 } 564 return tileWidth; 565 } 566 private TileSizeProperty tileWidth; 567 private void invalidateTileWidth() { 568 if (tileWidth != null) { 569 tileWidth.invalidate(); 570 } else { 571 _tileWidth = -1; 572 } 573 } 574 575 public final double getTileWidth() { 576 if (tileWidth != null) { 577 return tileWidth.get(); 578 } 579 if (_tileWidth == -1) { 580 _tileWidth = computeTileWidth(); 581 } 582 return _tileWidth; 583 } 584 585 /** 586 * The actual height of each tile. This property is read-only. 587 */ 588 public final ReadOnlyDoubleProperty tileHeightProperty() { 589 if (tileHeight == null) { 590 tileHeight = new TileSizeProperty("tileHeight", _tileHeight) { 591 592 @Override 593 public double compute() { 594 return computeTileHeight(); 595 } 596 597 }; 598 } 599 return tileHeight; 600 } 601 private TileSizeProperty tileHeight; 602 private void invalidateTileHeight() { 603 if (tileHeight != null) { 604 tileHeight.invalidate(); 605 } else { 606 _tileHeight = -1; 607 } 608 } 609 610 public final double getTileHeight() { 611 if (tileHeight != null) { 612 return tileHeight.get(); 613 } 614 if (_tileHeight == -1) { 615 _tileHeight = computeTileHeight(); 616 } 617 return _tileHeight; 618 } 619 620 /** 621 * The amount of horizontal space between each tile in a row. 622 */ 623 public final DoubleProperty hgapProperty() { 624 if (hgap == null) { 625 hgap = new StyleableDoubleProperty() { 626 @Override 627 public void invalidated() { 628 requestLayout(); 629 } 630 631 @Override 632 public CssMetaData<TilePane, Number> getCssMetaData() { 633 return StyleableProperties.HGAP; 634 } 635 636 @Override 637 public Object getBean() { 638 return TilePane.this; 639 } 640 641 @Override 642 public String getName() { 643 return "hgap"; 644 } 645 }; 646 } 647 return hgap; 648 } 649 650 private DoubleProperty hgap; 651 public final void setHgap(double value) { hgapProperty().set(value); } 652 public final double getHgap() { return hgap == null ? 0 : hgap.get(); } 653 654 /** 655 * The amount of vertical space between each tile in a column. 656 */ 657 public final DoubleProperty vgapProperty() { 658 if (vgap == null) { 659 vgap = new StyleableDoubleProperty() { 660 @Override 661 public void invalidated() { 662 requestLayout(); 663 } 664 665 @Override 666 public CssMetaData<TilePane, Number> getCssMetaData() { 667 return StyleableProperties.VGAP; 668 } 669 670 @Override 671 public Object getBean() { 672 return TilePane.this; 673 } 674 675 @Override 676 public String getName() { 677 return "vgap"; 678 } 679 }; 680 } 681 return vgap; 682 } 683 684 private DoubleProperty vgap; 685 public final void setVgap(double value) { vgapProperty().set(value); } 686 public final double getVgap() { return vgap == null ? 0 : vgap.get(); } 687 688 /** 689 * The overall alignment of the tilepane's content within its width and height. 690 * <p>For a horizontal tilepane, each row will be aligned within the tilepane's width 691 * using the alignment's hpos value, and the rows will be aligned within the 692 * tilepane's height using the alignment's vpos value. 693 * <p>For a vertical tilepane, each column will be aligned within the tilepane's height 694 * using the alignment's vpos value, and the columns will be aligned within the 695 * tilepane's width using the alignment's hpos value. 696 * 697 */ 698 public final ObjectProperty<Pos> alignmentProperty() { 699 if (alignment == null) { 700 alignment = new StyleableObjectProperty<Pos>(Pos.TOP_LEFT) { 701 @Override 702 public void invalidated() { 703 requestLayout(); 704 } 705 706 @Override 707 public CssMetaData<TilePane, Pos> getCssMetaData() { 708 return StyleableProperties.ALIGNMENT; 709 } 710 711 @Override 712 public Object getBean() { 713 return TilePane.this; 714 } 715 716 @Override 717 public String getName() { 718 return "alignment"; 719 } 720 }; 721 } 722 return alignment; 723 } 724 725 private ObjectProperty<Pos> alignment; 726 public final void setAlignment(Pos value) { alignmentProperty().set(value); } 727 public final Pos getAlignment() { return alignment == null ? Pos.TOP_LEFT : alignment.get(); } 728 private Pos getAlignmentInternal() { 729 Pos localPos = getAlignment(); 730 return localPos == null ? Pos.TOP_LEFT : localPos; 731 } 732 733 /** 734 * The default alignment of each child within its tile. 735 * This may be overridden on individual children by setting the child's 736 * alignment constraint. 737 */ 738 public final ObjectProperty<Pos> tileAlignmentProperty() { 739 if (tileAlignment == null) { 740 tileAlignment = new StyleableObjectProperty<Pos>(Pos.CENTER) { 741 @Override 742 public void invalidated() { 743 requestLayout(); 744 } 745 746 @Override 747 public CssMetaData<TilePane, Pos> getCssMetaData() { 748 return StyleableProperties.TILE_ALIGNMENT; 749 } 750 751 @Override 752 public Object getBean() { 753 return TilePane.this; 754 } 755 756 @Override 757 public String getName() { 758 return "tileAlignment"; 759 } 760 }; 761 } 762 return tileAlignment; 763 } 764 765 private ObjectProperty<Pos> tileAlignment; 766 public final void setTileAlignment(Pos value) { tileAlignmentProperty().set(value); } 767 public final Pos getTileAlignment() { return tileAlignment == null ? Pos.CENTER : tileAlignment.get(); } 768 private Pos getTileAlignmentInternal() { 769 Pos localPos = getTileAlignment(); 770 return localPos == null ? Pos.CENTER : localPos; 771 } 772 773 @Override public Orientation getContentBias() { 774 return getOrientation(); 775 } 776 777 @Override public void requestLayout() { 778 invalidateTileWidth(); 779 invalidateTileHeight(); 780 super.requestLayout(); 781 } 782 783 @Override protected double computeMinWidth(double height) { 784 if (getContentBias() == Orientation.HORIZONTAL) { 785 return getInsets().getLeft() + getTileWidth() + getInsets().getRight(); 786 } 787 return computePrefWidth(height); 788 } 789 790 @Override protected double computeMinHeight(double width) { 791 if (getContentBias() == Orientation.VERTICAL) { 792 return getInsets().getTop() + getTileHeight() + getInsets().getBottom(); 793 } 794 return computePrefHeight(width); 795 } 796 797 @Override protected double computePrefWidth(double forHeight) { 798 List<Node> managed = getManagedChildren(); 799 final Insets insets = getInsets(); 800 int prefCols = 0; 801 if (forHeight != -1) { 802 // first compute number of rows that will fit in given height and 803 // compute pref columns from that 804 int prefRows = computeRows(forHeight - snapSpace(insets.getTop()) - snapSpace(insets.getBottom()), getTileHeight()); 805 prefCols = computeOther(managed.size(), prefRows); 806 } else { 807 prefCols = getOrientation() == HORIZONTAL? getPrefColumns() : computeOther(managed.size(), getPrefRows()); 808 } 809 return snapSpace(insets.getLeft()) + 810 computeContentWidth(prefCols, getTileWidth()) + 811 snapSpace(insets.getRight()); 812 } 813 814 @Override protected double computePrefHeight(double forWidth) { 815 List<Node> managed = getManagedChildren(); 816 final Insets insets = getInsets(); 817 int prefRows = 0; 818 if (forWidth != -1) { 819 // first compute number of columns that will fit in given width and 820 // compute pref rows from that 821 int prefCols = computeColumns(forWidth - snapSpace(insets.getLeft()) - snapSpace(insets.getRight()), getTileWidth()); 822 prefRows = computeOther(managed.size(), prefCols); 823 } else { 824 prefRows = getOrientation() == HORIZONTAL? computeOther(managed.size(), getPrefColumns()) : getPrefRows(); 825 } 826 return snapSpace(insets.getTop()) + 827 computeContentHeight(prefRows, getTileHeight()) + 828 snapSpace(insets.getBottom()); 829 } 830 831 private double computeTileWidth() { 832 List<Node> managed = getManagedChildren(); 833 double preftilewidth = getPrefTileWidth(); 834 if (preftilewidth == USE_COMPUTED_SIZE) { 835 double h = -1; 836 boolean vertBias = false; 837 for (int i = 0, size = managed.size(); i < size; i++) { 838 Node child = managed.get(i); 839 if (child.getContentBias() == VERTICAL) { 840 vertBias = true; 841 break; 842 } 843 } 844 if (vertBias) { 845 // widest may depend on height of tile 846 h = computeMaxPrefAreaHeight(managed, marginAccessor, -1, getTileAlignmentInternal().getVpos()); 847 } 848 return snapSize(computeMaxPrefAreaWidth(managed, marginAccessor, h, true)); 849 } 850 return snapSize(preftilewidth); 851 } 852 853 private double computeTileHeight() { 854 List<Node> managed = getManagedChildren(); 855 double preftileheight = getPrefTileHeight(); 856 if (preftileheight == USE_COMPUTED_SIZE) { 857 double w = -1; 858 boolean horizBias = false; 859 for (int i = 0, size = managed.size(); i < size; i++) { 860 Node child = managed.get(i); 861 if (child.getContentBias() == Orientation.HORIZONTAL) { 862 horizBias = true; 863 break; 864 } 865 } 866 if (horizBias) { 867 // tallest may depend on width of tile 868 w = computeMaxPrefAreaWidth(managed, marginAccessor); 869 } 870 return snapSize(computeMaxPrefAreaHeight(managed, marginAccessor, w, getTileAlignmentInternal().getVpos())); 871 } 872 return snapSize(preftileheight); 873 } 874 875 private int computeOther(int numNodes, int numCells) { 876 double other = (double)numNodes/(double)Math.max(1, numCells); 877 return (int)Math.ceil(other); 878 } 879 880 private int computeColumns(double width, double tilewidth) { 881 return Math.max(1,(int)((width + snapSpace(getHgap())) / (tilewidth + snapSpace(getHgap())))); 882 } 883 884 private int computeRows(double height, double tileheight) { 885 return Math.max(1, (int)((height + snapSpace(getVgap())) / (tileheight + snapSpace(getVgap())))); 886 } 887 888 private double computeContentWidth(int columns, double tilewidth) { 889 if (columns == 0) return 0; 890 return columns * tilewidth + (columns - 1) * snapSpace(getHgap()); 891 } 892 893 private double computeContentHeight(int rows, double tileheight) { 894 if (rows == 0) return 0; 895 return rows * tileheight + (rows - 1) * snapSpace(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 = snapSpace(getInsets().getTop()); 905 double left = snapSpace(getInsets().getLeft()); 906 double bottom = snapSpace(getInsets().getBottom()); 907 double right = snapSpace(getInsets().getRight()); 908 double vgap = snapSpace(getVgap()); 909 double hgap = snapSpace(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 }