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 }