1 /*
   2  * Copyright (c) 2011, 2016, 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 &lt; 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 &lt; 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 &lt; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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       */
 995      private static class StyleableProperties {
 996 
 997          private static final CssMetaData<TilePane,Pos> ALIGNMENT =
 998              new CssMetaData<TilePane,Pos>("-fx-alignment",
 999                  new EnumConverter<Pos>(Pos.class),
1000                  Pos.TOP_LEFT) {
1001 
1002             @Override
1003             public boolean isSettable(TilePane node) {
1004                 return node.alignment == null || !node.alignment.isBound();
1005             }
1006 
1007             @Override
1008             public StyleableProperty<Pos> getStyleableProperty(TilePane node) {
1009                 return (StyleableProperty<Pos>)node.alignmentProperty();
1010             }
1011         };
1012 
1013          private static final CssMetaData<TilePane,Number> PREF_COLUMNS =
1014              new CssMetaData<TilePane,Number>("-fx-pref-columns",
1015                  SizeConverter.getInstance(), 5.0) {
1016 
1017             @Override
1018             public boolean isSettable(TilePane node) {
1019                 return node.prefColumns == null ||
1020                         !node.prefColumns.isBound();
1021             }
1022 
1023             @Override
1024             public StyleableProperty<Number> getStyleableProperty(TilePane node) {
1025                 return (StyleableProperty<Number>)node.prefColumnsProperty();
1026             }
1027         };
1028 
1029          private static final CssMetaData<TilePane,Number> HGAP =
1030              new CssMetaData<TilePane,Number>("-fx-hgap",
1031                  SizeConverter.getInstance(), 0.0) {
1032 
1033             @Override
1034             public boolean isSettable(TilePane node) {
1035                 return node.hgap == null ||
1036                         !node.hgap.isBound();
1037             }
1038 
1039             @Override
1040             public StyleableProperty<Number> getStyleableProperty(TilePane node) {
1041                 return (StyleableProperty<Number>)node.hgapProperty();
1042             }
1043         };
1044 
1045          private static final CssMetaData<TilePane,Number> PREF_ROWS =
1046              new CssMetaData<TilePane,Number>("-fx-pref-rows",
1047                  SizeConverter.getInstance(), 5.0) {
1048 
1049             @Override
1050             public boolean isSettable(TilePane node) {
1051                 return node.prefRows == null ||
1052                         !node.prefRows.isBound();
1053             }
1054 
1055             @Override
1056             public StyleableProperty<Number> getStyleableProperty(TilePane node) {
1057                 return (StyleableProperty<Number>)node.prefRowsProperty();
1058             }
1059         };
1060 
1061          private static final CssMetaData<TilePane,Pos> TILE_ALIGNMENT =
1062              new CssMetaData<TilePane,Pos>("-fx-tile-alignment",
1063                  new EnumConverter<Pos>(Pos.class),
1064                  Pos.CENTER) {
1065 
1066             @Override
1067             public boolean isSettable(TilePane node) {
1068                 return node.tileAlignment == null ||
1069                         !node.tileAlignment.isBound();
1070             }
1071 
1072             @Override
1073             public StyleableProperty<Pos> getStyleableProperty(TilePane node) {
1074                 return (StyleableProperty<Pos>)node.tileAlignmentProperty();
1075             }
1076          };
1077 
1078          private static final CssMetaData<TilePane,Number> PREF_TILE_WIDTH =
1079              new CssMetaData<TilePane,Number>("-fx-pref-tile-width",
1080                  SizeConverter.getInstance(), USE_COMPUTED_SIZE) {
1081 
1082             @Override
1083             public boolean isSettable(TilePane node) {
1084                 return node.prefTileWidth == null ||
1085                         !node.prefTileWidth.isBound();
1086             }
1087 
1088             @Override
1089             public StyleableProperty<Number> getStyleableProperty(TilePane node) {
1090                 return (StyleableProperty<Number>)node.prefTileWidthProperty();
1091             }
1092         };
1093 
1094          private static final CssMetaData<TilePane,Number> PREF_TILE_HEIGHT =
1095              new CssMetaData<TilePane,Number>("-fx-pref-tile-height",
1096                  SizeConverter.getInstance(), USE_COMPUTED_SIZE) {
1097 
1098             @Override
1099             public boolean isSettable(TilePane node) {
1100                 return node.prefTileHeight == null ||
1101                         !node.prefTileHeight.isBound();
1102             }
1103 
1104             @Override
1105             public StyleableProperty<Number> getStyleableProperty(TilePane node) {
1106                 return (StyleableProperty<Number>)node.prefTileHeightProperty();
1107             }
1108          };
1109 
1110          private static final CssMetaData<TilePane,Orientation> ORIENTATION =
1111              new CssMetaData<TilePane,Orientation>("-fx-orientation",
1112                  new EnumConverter<Orientation>(Orientation.class),
1113                  Orientation.HORIZONTAL) {
1114 
1115                 @Override
1116                 public Orientation getInitialValue(TilePane node) {
1117                     // A vertical TilePane should remain vertical
1118                     return node.getOrientation();
1119                 }
1120 
1121                 @Override
1122                 public boolean isSettable(TilePane node) {
1123                     return node.orientation == null ||
1124                             !node.orientation.isBound();
1125                 }
1126 
1127                 @Override
1128                 public StyleableProperty<Orientation> getStyleableProperty(TilePane node) {
1129                     return (StyleableProperty<Orientation>)node.orientationProperty();
1130                 }
1131          };
1132 
1133          private static final CssMetaData<TilePane,Number> VGAP =
1134              new CssMetaData<TilePane,Number>("-fx-vgap",
1135                  SizeConverter.getInstance(), 0.0) {
1136 
1137             @Override
1138             public boolean isSettable(TilePane node) {
1139                 return node.vgap == null ||
1140                         !node.vgap.isBound();
1141             }
1142 
1143             @Override
1144             public StyleableProperty<Number> getStyleableProperty(TilePane node) {
1145                 return (StyleableProperty<Number>)node.vgapProperty();
1146             }
1147         };
1148 
1149          private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
1150          static {
1151             final List<CssMetaData<? extends Styleable, ?>> styleables =
1152                 new ArrayList<CssMetaData<? extends Styleable, ?>>(Region.getClassCssMetaData());
1153             styleables.add(ALIGNMENT);
1154             styleables.add(HGAP);
1155             styleables.add(ORIENTATION);
1156             styleables.add(PREF_COLUMNS);
1157             styleables.add(PREF_ROWS);
1158             styleables.add(PREF_TILE_WIDTH);
1159             styleables.add(PREF_TILE_HEIGHT);
1160             styleables.add(TILE_ALIGNMENT);
1161             styleables.add(VGAP);
1162             STYLEABLES = Collections.unmodifiableList(styleables);
1163          }
1164     }
1165 
1166     /**
1167      * @return The CssMetaData associated with this class, which may include the
1168      * CssMetaData of its super classes.
1169      * @since JavaFX 8.0
1170      */
1171     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
1172         return StyleableProperties.STYLEABLES;
1173     }
1174 
1175     /**
1176      * {@inheritDoc}
1177      *
1178      * @since JavaFX 8.0
1179      */
1180 
1181 
1182     @Override
1183     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
1184         return getClassCssMetaData();
1185     }
1186 
1187     private abstract class TileSizeProperty extends ReadOnlyDoubleProperty {
1188         private final String name;
1189         private ExpressionHelper<Number> helper;
1190         private double value;
1191         private boolean valid;
1192 
1193         TileSizeProperty(String name, double initSize) {
1194             this.name = name;
1195             this.value = initSize;
1196             this.valid = initSize != -1;
1197         }
1198 
1199 
1200         @Override
1201         public Object getBean() {
1202             return TilePane.this;
1203         }
1204 
1205         @Override
1206         public String getName() {
1207             return name;
1208         }
1209 
1210         @Override
1211         public void addListener(InvalidationListener listener) {
1212             helper = ExpressionHelper.addListener(helper, this, listener);
1213         }
1214 
1215         @Override
1216         public void removeListener(InvalidationListener listener) {
1217             helper = ExpressionHelper.removeListener(helper, listener);
1218         }
1219 
1220         @Override
1221         public void addListener(ChangeListener<? super Number> listener) {
1222             helper = ExpressionHelper.addListener(helper, this, listener);
1223         }
1224 
1225         @Override
1226         public void removeListener(ChangeListener<? super Number> listener) {
1227             helper = ExpressionHelper.removeListener(helper, listener);
1228         }
1229 
1230         @Override
1231         public double get() {
1232             if (!valid) {
1233                 value = compute();
1234                 valid = true;
1235             }
1236 
1237             return value;
1238         }
1239 
1240         public void invalidate() {
1241             if (valid) {
1242                 valid = false;
1243                 ExpressionHelper.fireValueChangedEvent(helper);
1244             }
1245         }
1246 
1247         public abstract double compute();
1248     }
1249 
1250 }