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