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