/* * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javafx.scene.layout; import com.sun.javafx.binding.ExpressionHelper; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javafx.beans.property.DoubleProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.css.CssMetaData; import javafx.css.StyleableDoubleProperty; import javafx.css.StyleableIntegerProperty; import javafx.css.StyleableObjectProperty; import javafx.css.StyleableProperty; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.geometry.VPos; import javafx.scene.Node; import javafx.css.converter.EnumConverter; import javafx.css.converter.SizeConverter; import javafx.beans.InvalidationListener; import javafx.beans.value.ChangeListener; import javafx.css.Styleable; import static javafx.geometry.Orientation.*; import javafx.util.Callback; /** * TilePane lays out its children in a grid of uniformly sized "tiles". *

* A horizontal tilepane (the default) will tile nodes in rows, wrapping at the * tilepane's width. A vertical tilepane will tile nodes in columns, * wrapping at the tilepane's height. *

* The size of each "tile" defaults to the size needed to encompass the largest * preferred width and height of the tilepane's children and the tilepane * will recompute the size of the tiles as needed to accommodate the largest preferred * size of its children as it changes. The application may also control the size * of the tiles directly by setting prefTileWidth/prefTileHeight * properties to a value other than USE_COMPUTED_SIZE (the default). *

* Applications should initialize either prefColumns (for horizontal) * or prefRows (for vertical) to establish the tilepane's preferred * size (the arbitrary default is 5). Note that prefColumns/prefRows * is used only for calculating the preferred size and may not reflect the actual * number of rows or columns, which may change as the tilepane is resized and * the tiles are wrapped at its actual boundaries. *

* The alignment property controls how the rows and columns are aligned * within the bounds of the tilepane and defaults to Pos.TOP_LEFT. It is also possible * to control the alignment of nodes within the individual tiles by setting * {@link #tileAlignmentProperty() tileAlignment}, which defaults to Pos.CENTER. *

* A horizontal tilepane example: *


 *    TilePane tile = new TilePane();
 *    tile.setHgap(8);
 *    tile.setPrefColumns(4);
 *    for (int i = 0; i < 20; i++) {
 *        tile.getChildren().add(new ImageView(...));
 *    }
 * 
*

* A vertical TilePane example: *


 *    TilePane tile = new TilePane(Orientation.VERTICAL);
 *    tile.setTileAlignment(Pos.CENTER_LEFT);
 *    tile.setPrefRows(10);
 *    for (int i = 0; i < 50; i++) {
 *        tile.getChildren().add(new ImageView(...));
 *    }
 * 
* * The TilePane will attempt to resize each child to fill its tile. * If the child could not be sized to fill the tile (either because it was not * resizable or its size limits prevented it) then it will be aligned within the * tile using tileAlignment. * *

Resizable Range

* * A tilepane's parent will resize the tilepane within the tilepane's resizable range * during layout. By default the tilepane computes this range based on its content * as outlined in the tables below. *

* Horizontal: * * * * * * * * * * *
widthheight
minimumleft/right insets plus the tile width.top/bottom insets plus height required to display all tiles when wrapped at a specified width with a vgap between each row.
preferredleft/right insets plus prefColumns multiplied by the tile width.top/bottom insets plus height required to display all tiles when wrapped at a specified width with a vgap between each row.
maximumDouble.MAX_VALUEDouble.MAX_VALUE
*

* Vertical: * * * * * * * * * * *
widthheight
minimumleft/right insets plus width required to display all tiles when wrapped at a specified height with an hgap between each column.top/bottom insets plus the tile height.
preferredleft/right insets plus width required to display all tiles when wrapped at the specified height with an hgap between each column.top/bottom insets plus prefRows multiplied by the tile height.
maximumDouble.MAX_VALUEDouble.MAX_VALUE
*

* A tilepane's unbounded maximum width and height are an indication to the parent that * it may be resized beyond its preferred size to fill whatever space is assigned to it. *

* TilePane provides properties for setting the size range directly. These * properties default to the sentinel value Region.USE_COMPUTED_SIZE, however the * application may set them to other values as needed: *


 *     tilepane.setMaxWidth(500);
 * 
* Applications may restore the computed values by setting these properties back * to Region.USE_COMPUTED_SIZE. *

* TilePane does not clip its content by default, so it is possible that childrens' * bounds may extend outside the tiles (and possibly the tilepane bounds) if a * child's pref size prevents it from being fit within its tile. Also, if the tilepane * is resized smaller than its preferred size, it may not be able to fit all the * tiles within its bounds and the content will extend outside. * *

Optional Layout Constraints

* * An application may set constraints on individual children to customize TilePane's layout. * For each constraint, TilePane provides a static method for setting it on the child. *

* * * * *
ConstraintTypeDescription
alignmentjavafx.geometry.PosThe alignment of the child within its tile.
marginjavafx.geometry.InsetsMargin space around the outside of the child.
*

* Example: *


 *     TilePane tilepane = new TilePane();
 *     for (int i = 0; i < 20; i++) {
 *        Label title = new Label(imageTitle[i]):
 *        Imageview imageview = new ImageView(new Image(imageName[i]));
 *        TilePane.setAlignment(label, Pos.BOTTOM_RIGHT);
 *        tilepane.getChildren().addAll(title, imageview);
 *     }
 * 
* @since JavaFX 2.0 */ public class TilePane extends Pane { /******************************************************************** * BEGIN static methods ********************************************************************/ private static final String MARGIN_CONSTRAINT = "tilepane-margin"; private static final String ALIGNMENT_CONSTRAINT = "tilepane-alignment"; /** * Sets the alignment for the child when contained by a tilepane. * If set, will override the tilepane's default alignment for children * within their 'tiles'. * Setting the value to null will remove the constraint. * @param node the child node of a tilepane * @param value the alignment position for the child */ public static void setAlignment(Node node, Pos value) { setConstraint(node, ALIGNMENT_CONSTRAINT, value); } /** * Returns the child's alignment constraint if set. * @param node the child node of a tilepane * @return the alignment position for the child or null if no alignment was set */ public static Pos getAlignment(Node node) { return (Pos)getConstraint(node, ALIGNMENT_CONSTRAINT); } /** * Sets the margin for the child when contained by a tilepane. * If set, the tilepane will layout the child with the margin space around it. * Setting the value to null will remove the constraint. * @param node the child node of a tilepane * @param value the margin of space around the child */ public static void setMargin(Node node, Insets value) { setConstraint(node, MARGIN_CONSTRAINT, value); } /** * Returns the child's margin constraint if set. * @param node the child node of a tilepane * @return the margin for the child or null if no margin was set */ public static Insets getMargin(Node node) { return (Insets)getConstraint(node, MARGIN_CONSTRAINT); } private static final Callback marginAccessor = n -> getMargin(n); /** * Removes all tilepane constraints from the child node. * @param child the child node */ public static void clearConstraints(Node child) { setAlignment(child, null); setMargin(child, null); } /******************************************************************** * END static methods ********************************************************************/ private double _tileWidth = -1; private double _tileHeight = -1; /** * Creates a horizontal TilePane layout with prefColumn = 5 and hgap/vgap = 0. */ public TilePane() { super(); } /** * Creates a TilePane layout with the specified orientation, * prefColumn/prefRows = 5 and hgap/vgap = 0. * @param orientation the direction the tiles should flow & wrap */ public TilePane(Orientation orientation) { super(); setOrientation(orientation); } /** * Creates a horizontal TilePane layout with prefColumn = 5 and the specified * hgap/vgap. * @param hgap the amount of horizontal space between each tile * @param vgap the amount of vertical space between each tile */ public TilePane(double hgap, double vgap) { super(); setHgap(hgap); setVgap(vgap); } /** * Creates a TilePane layout with the specified orientation, hgap/vgap, * and prefRows/prefColumns = 5. * @param orientation the direction the tiles should flow & wrap * @param hgap the amount of horizontal space between each tile * @param vgap the amount of vertical space between each tile */ public TilePane(Orientation orientation, double hgap, double vgap) { this(); setOrientation(orientation); setHgap(hgap); setVgap(vgap); } /** * Creates a horizontal TilePane layout with prefColumn = 5 and hgap/vgap = 0. * @param children The initial set of children for this pane. * @since JavaFX 8.0 */ public TilePane(Node... children) { super(); getChildren().addAll(children); } /** * Creates a TilePane layout with the specified orientation, * prefColumn/prefRows = 5 and hgap/vgap = 0. * @param orientation the direction the tiles should flow & wrap * @param children The initial set of children for this pane. * @since JavaFX 8.0 */ public TilePane(Orientation orientation, Node... children) { super(); setOrientation(orientation); getChildren().addAll(children); } /** * Creates a horizontal TilePane layout with prefColumn = 5 and the specified * hgap/vgap. * @param hgap the amount of horizontal space between each tile * @param vgap the amount of vertical space between each tile * @param children The initial set of children for this pane. * @since JavaFX 8.0 */ public TilePane(double hgap, double vgap, Node... children) { super(); setHgap(hgap); setVgap(vgap); getChildren().addAll(children); } /** * Creates a TilePane layout with the specified orientation, hgap/vgap, * and prefRows/prefColumns = 5. * @param orientation the direction the tiles should flow & wrap * @param hgap the amount of horizontal space between each tile * @param vgap the amount of vertical space between each tile * @param children The initial set of children for this pane. * @since JavaFX 8.0 */ public TilePane(Orientation orientation, double hgap, double vgap, Node... children) { this(); setOrientation(orientation); setHgap(hgap); setVgap(vgap); getChildren().addAll(children); } /** * The orientation of this tilepane. * A horizontal tilepane lays out children in tiles, left to right, wrapping * tiles at the tilepane's width boundary. A vertical tilepane lays out * children in tiles, top to bottom, wrapping at the tilepane's height. * The default is horizontal. */ public final ObjectProperty orientationProperty() { if (orientation == null) { orientation = new StyleableObjectProperty(HORIZONTAL) { @Override public void invalidated() { requestLayout(); } @Override public CssMetaData getCssMetaData() { return StyleableProperties.ORIENTATION; } @Override public Object getBean() { return TilePane.this; } @Override public String getName() { return "orientation"; } }; } return orientation; } private ObjectProperty orientation; public final void setOrientation(Orientation value) { orientationProperty().set(value); } public final Orientation getOrientation() { return orientation == null ? HORIZONTAL : orientation.get(); } /** * The preferred number of rows for a vertical tilepane. * This value is used only to compute the preferred size of the tilepane * and may not reflect the actual number of rows, which may change * if the tilepane is resized to something other than its preferred height. * This property is ignored for a horizontal tilepane. *

* It is recommended that the application initialize this value for a * vertical tilepane. */ public final IntegerProperty prefRowsProperty() { if (prefRows == null) { prefRows = new StyleableIntegerProperty(5) { @Override public void invalidated() { requestLayout(); } @Override public CssMetaData getCssMetaData() { return StyleableProperties.PREF_ROWS; } @Override public Object getBean() { return TilePane.this; } @Override public String getName() { return "prefRows"; } }; } return prefRows; } private IntegerProperty prefRows; public final void setPrefRows(int value) { prefRowsProperty().set(value); } public final int getPrefRows() { return prefRows == null ? 5 : prefRows.get(); } /** * The preferred number of columns for a horizontal tilepane. * This value is used only to compute the preferred size of the tilepane * and may not reflect the actual number of rows, which may change if the * tilepane is resized to something other than its preferred height. * This property is ignored for a vertical tilepane. *

* It is recommended that the application initialize this value for a * horizontal tilepane. */ public final IntegerProperty prefColumnsProperty() { if (prefColumns == null) { prefColumns = new StyleableIntegerProperty(5) { @Override public void invalidated() { requestLayout(); } @Override public CssMetaData getCssMetaData() { return StyleableProperties.PREF_COLUMNS; } @Override public Object getBean() { return TilePane.this; } @Override public String getName() { return "prefColumns"; } }; } return prefColumns; } private IntegerProperty prefColumns; public final void setPrefColumns(int value) { prefColumnsProperty().set(value); } public final int getPrefColumns() { return prefColumns == null ? 5 : prefColumns.get(); } /** * The preferred width of each tile. * If equal to USE_COMPUTED_SIZE (the default) the tile width wlll be * automatically recomputed by the tilepane when the preferred size of children * changes to accommodate the widest child. If the application sets this property * to value greater than 0, then tiles will be set to that width and the tilepane * will attempt to resize children to fit within that width (if they are resizable and * their min-max width range allows it). */ public final DoubleProperty prefTileWidthProperty() { if (prefTileWidth == null) { prefTileWidth = new StyleableDoubleProperty(USE_COMPUTED_SIZE) { @Override public void invalidated() { requestLayout(); } @Override public CssMetaData getCssMetaData() { return StyleableProperties.PREF_TILE_WIDTH; } @Override public Object getBean() { return TilePane.this; } @Override public String getName() { return "prefTileWidth"; } }; } return prefTileWidth; } private DoubleProperty prefTileWidth; public final void setPrefTileWidth(double value) { prefTileWidthProperty().set(value); } public final double getPrefTileWidth() { return prefTileWidth == null ? USE_COMPUTED_SIZE : prefTileWidth.get(); } /** * The preferred height of each tile. * If equal to USE_COMPUTED_SIZE (the default) the tile height wlll be * automatically recomputed by the tilepane when the preferred size of children * changes to accommodate the tallest child. If the application sets this property * to value greater than 0, then tiles will be set to that height and the tilepane * will attempt to resize children to fit within that height (if they are resizable and * their min-max height range allows it). */ public final DoubleProperty prefTileHeightProperty() { if (prefTileHeight == null) { prefTileHeight = new StyleableDoubleProperty(USE_COMPUTED_SIZE) { @Override public void invalidated() { requestLayout(); } @Override public CssMetaData getCssMetaData() { return StyleableProperties.PREF_TILE_HEIGHT; } @Override public Object getBean() { return TilePane.this; } @Override public String getName() { return "prefTileHeight"; } }; } return prefTileHeight; } private DoubleProperty prefTileHeight; public final void setPrefTileHeight(double value) { prefTileHeightProperty().set(value); } public final double getPrefTileHeight() { return prefTileHeight == null ? USE_COMPUTED_SIZE : prefTileHeight.get(); } /** * The actual width of each tile. This property is read-only. */ public final ReadOnlyDoubleProperty tileWidthProperty() { if (tileWidth == null) { tileWidth = new TileSizeProperty("tileWidth", _tileWidth) { @Override public double compute() { return computeTileWidth(); } }; } return tileWidth; } private TileSizeProperty tileWidth; private void invalidateTileWidth() { if (tileWidth != null) { tileWidth.invalidate(); } else { _tileWidth = -1; } } public final double getTileWidth() { if (tileWidth != null) { return tileWidth.get(); } if (_tileWidth == -1) { _tileWidth = computeTileWidth(); } return _tileWidth; } /** * The actual height of each tile. This property is read-only. */ public final ReadOnlyDoubleProperty tileHeightProperty() { if (tileHeight == null) { tileHeight = new TileSizeProperty("tileHeight", _tileHeight) { @Override public double compute() { return computeTileHeight(); } }; } return tileHeight; } private TileSizeProperty tileHeight; private void invalidateTileHeight() { if (tileHeight != null) { tileHeight.invalidate(); } else { _tileHeight = -1; } } public final double getTileHeight() { if (tileHeight != null) { return tileHeight.get(); } if (_tileHeight == -1) { _tileHeight = computeTileHeight(); } return _tileHeight; } /** * The amount of horizontal space between each tile in a row. */ public final DoubleProperty hgapProperty() { if (hgap == null) { hgap = new StyleableDoubleProperty() { @Override public void invalidated() { requestLayout(); } @Override public CssMetaData getCssMetaData() { return StyleableProperties.HGAP; } @Override public Object getBean() { return TilePane.this; } @Override public String getName() { return "hgap"; } }; } return hgap; } private DoubleProperty hgap; public final void setHgap(double value) { hgapProperty().set(value); } public final double getHgap() { return hgap == null ? 0 : hgap.get(); } /** * The amount of vertical space between each tile in a column. */ public final DoubleProperty vgapProperty() { if (vgap == null) { vgap = new StyleableDoubleProperty() { @Override public void invalidated() { requestLayout(); } @Override public CssMetaData getCssMetaData() { return StyleableProperties.VGAP; } @Override public Object getBean() { return TilePane.this; } @Override public String getName() { return "vgap"; } }; } return vgap; } private DoubleProperty vgap; public final void setVgap(double value) { vgapProperty().set(value); } public final double getVgap() { return vgap == null ? 0 : vgap.get(); } /** * The overall alignment of the tilepane's content within its width and height. *

For a horizontal tilepane, each row will be aligned within the tilepane's width * using the alignment's hpos value, and the rows will be aligned within the * tilepane's height using the alignment's vpos value. *

For a vertical tilepane, each column will be aligned within the tilepane's height * using the alignment's vpos value, and the columns will be aligned within the * tilepane's width using the alignment's hpos value. * */ public final ObjectProperty alignmentProperty() { if (alignment == null) { alignment = new StyleableObjectProperty(Pos.TOP_LEFT) { @Override public void invalidated() { requestLayout(); } @Override public CssMetaData getCssMetaData() { return StyleableProperties.ALIGNMENT; } @Override public Object getBean() { return TilePane.this; } @Override public String getName() { return "alignment"; } }; } return alignment; } private ObjectProperty alignment; public final void setAlignment(Pos value) { alignmentProperty().set(value); } public final Pos getAlignment() { return alignment == null ? Pos.TOP_LEFT : alignment.get(); } private Pos getAlignmentInternal() { Pos localPos = getAlignment(); return localPos == null ? Pos.TOP_LEFT : localPos; } /** * The default alignment of each child within its tile. * This may be overridden on individual children by setting the child's * alignment constraint. */ public final ObjectProperty tileAlignmentProperty() { if (tileAlignment == null) { tileAlignment = new StyleableObjectProperty(Pos.CENTER) { @Override public void invalidated() { requestLayout(); } @Override public CssMetaData getCssMetaData() { return StyleableProperties.TILE_ALIGNMENT; } @Override public Object getBean() { return TilePane.this; } @Override public String getName() { return "tileAlignment"; } }; } return tileAlignment; } private ObjectProperty tileAlignment; public final void setTileAlignment(Pos value) { tileAlignmentProperty().set(value); } public final Pos getTileAlignment() { return tileAlignment == null ? Pos.CENTER : tileAlignment.get(); } private Pos getTileAlignmentInternal() { Pos localPos = getTileAlignment(); return localPos == null ? Pos.CENTER : localPos; } @Override public Orientation getContentBias() { return getOrientation(); } @Override public void requestLayout() { invalidateTileWidth(); invalidateTileHeight(); super.requestLayout(); } @Override protected double computeMinWidth(double height) { if (getContentBias() == Orientation.HORIZONTAL) { return getInsets().getLeft() + getTileWidth() + getInsets().getRight(); } return computePrefWidth(height); } @Override protected double computeMinHeight(double width) { if (getContentBias() == Orientation.VERTICAL) { return getInsets().getTop() + getTileHeight() + getInsets().getBottom(); } return computePrefHeight(width); } @Override protected double computePrefWidth(double forHeight) { List managed = getManagedChildren(); final Insets insets = getInsets(); int prefCols = 0; if (forHeight != -1) { // first compute number of rows that will fit in given height and // compute pref columns from that int prefRows = computeRows(forHeight - snapSpaceY(insets.getTop()) - snapSpaceY(insets.getBottom()), getTileHeight()); prefCols = computeOther(managed.size(), prefRows); } else { prefCols = getOrientation() == HORIZONTAL? getPrefColumns() : computeOther(managed.size(), getPrefRows()); } return snapSpaceX(insets.getLeft()) + computeContentWidth(prefCols, getTileWidth()) + snapSpaceX(insets.getRight()); } @Override protected double computePrefHeight(double forWidth) { List managed = getManagedChildren(); final Insets insets = getInsets(); int prefRows = 0; if (forWidth != -1) { // first compute number of columns that will fit in given width and // compute pref rows from that int prefCols = computeColumns(forWidth - snapSpaceX(insets.getLeft()) - snapSpaceX(insets.getRight()), getTileWidth()); prefRows = computeOther(managed.size(), prefCols); } else { prefRows = getOrientation() == HORIZONTAL? computeOther(managed.size(), getPrefColumns()) : getPrefRows(); } return snapSpaceY(insets.getTop()) + computeContentHeight(prefRows, getTileHeight()) + snapSpaceY(insets.getBottom()); } private double computeTileWidth() { List managed = getManagedChildren(); double preftilewidth = getPrefTileWidth(); if (preftilewidth == USE_COMPUTED_SIZE) { double h = -1; boolean vertBias = false; for (int i = 0, size = managed.size(); i < size; i++) { Node child = managed.get(i); if (child.getContentBias() == VERTICAL) { vertBias = true; break; } } if (vertBias) { // widest may depend on height of tile h = computeMaxPrefAreaHeight(managed, marginAccessor, -1, getTileAlignmentInternal().getVpos()); } return snapSizeX(computeMaxPrefAreaWidth(managed, marginAccessor, h, true)); } return snapSizeX(preftilewidth); } private double computeTileHeight() { List managed = getManagedChildren(); double preftileheight = getPrefTileHeight(); if (preftileheight == USE_COMPUTED_SIZE) { double w = -1; boolean horizBias = false; for (int i = 0, size = managed.size(); i < size; i++) { Node child = managed.get(i); if (child.getContentBias() == Orientation.HORIZONTAL) { horizBias = true; break; } } if (horizBias) { // tallest may depend on width of tile w = computeMaxPrefAreaWidth(managed, marginAccessor); } return snapSizeY(computeMaxPrefAreaHeight(managed, marginAccessor, w, getTileAlignmentInternal().getVpos())); } return snapSizeY(preftileheight); } private int computeOther(int numNodes, int numCells) { double other = (double)numNodes/(double)Math.max(1, numCells); return (int)Math.ceil(other); } private int computeColumns(double width, double tilewidth) { double snappedHgap = snapSpaceX(getHgap()); return Math.max(1,(int)((width + snappedHgap) / (tilewidth + snappedHgap))); } private int computeRows(double height, double tileheight) { double snappedVgap = snapSpaceY(getVgap()); return Math.max(1, (int)((height + snappedVgap) / (tileheight + snappedVgap))); } private double computeContentWidth(int columns, double tilewidth) { if (columns == 0) return 0; return columns * tilewidth + (columns - 1) * snapSpaceX(getHgap()); } private double computeContentHeight(int rows, double tileheight) { if (rows == 0) return 0; return rows * tileheight + (rows - 1) * snapSpaceY(getVgap()); } @Override protected void layoutChildren() { List managed = getManagedChildren(); HPos hpos = getAlignmentInternal().getHpos(); VPos vpos = getAlignmentInternal().getVpos(); double width = getWidth(); double height = getHeight(); double top = snapSpaceY(getInsets().getTop()); double left = snapSpaceX(getInsets().getLeft()); double bottom = snapSpaceY(getInsets().getBottom()); double right = snapSpaceX(getInsets().getRight()); double vgap = snapSpaceY(getVgap()); double hgap = snapSpaceX(getHgap()); double insideWidth = width - left - right; double insideHeight = height - top - bottom; double tileWidth = getTileWidth() > insideWidth ? insideWidth : getTileWidth(); double tileHeight = getTileHeight() > insideHeight ? insideHeight : getTileHeight(); int lastRowRemainder = 0; int lastColumnRemainder = 0; if (getOrientation() == HORIZONTAL) { actualColumns = computeColumns(insideWidth, tileWidth); actualRows = computeOther(managed.size(), actualColumns); // remainder will be 0 if last row is filled lastRowRemainder = hpos != HPos.LEFT? actualColumns - (actualColumns*actualRows - managed.size()) : 0; } else { // vertical actualRows = computeRows(insideHeight, tileHeight); actualColumns = computeOther(managed.size(), actualRows); // remainder will be 0 if last column is filled lastColumnRemainder = vpos != VPos.TOP? actualRows - (actualColumns*actualRows - managed.size()) : 0; } double rowX = left + computeXOffset(insideWidth, computeContentWidth(actualColumns, tileWidth), hpos); double columnY = top + computeYOffset(insideHeight, computeContentHeight(actualRows, tileHeight), vpos); double lastRowX = lastRowRemainder > 0? left + computeXOffset(insideWidth, computeContentWidth(lastRowRemainder, tileWidth), hpos) : rowX; double lastColumnY = lastColumnRemainder > 0? top + computeYOffset(insideHeight, computeContentHeight(lastColumnRemainder, tileHeight), vpos) : columnY; double baselineOffset = getTileAlignmentInternal().getVpos() == VPos.BASELINE ? getAreaBaselineOffset(managed, marginAccessor, i -> tileWidth, tileHeight, false) : -1; int r = 0; int c = 0; for (int i = 0, size = managed.size(); i < size; i++) { Node child = managed.get(i); double xoffset = r == (actualRows - 1)? lastRowX : rowX; double yoffset = c == (actualColumns - 1)? lastColumnY : columnY; double tileX = xoffset + (c * (tileWidth + hgap)); double tileY = yoffset + (r * (tileHeight + vgap)); Pos childAlignment = getAlignment(child); layoutInArea(child, tileX, tileY, tileWidth, tileHeight, baselineOffset, getMargin(child), childAlignment != null? childAlignment.getHpos() : getTileAlignmentInternal().getHpos(), childAlignment != null? childAlignment.getVpos() : getTileAlignmentInternal().getVpos()); if (getOrientation() == HORIZONTAL) { if (++c == actualColumns) { c = 0; r++; } } else { // vertical if (++r == actualRows) { r = 0; c++; } } } } private int actualRows = 0; private int actualColumns = 0; /*************************************************************************** * * * Stylesheet Handling * * * **************************************************************************/ /** * Super-lazy instantiation pattern from Bill Pugh. * @treatAsPrivate implementation detail */ private static class StyleableProperties { private static final CssMetaData ALIGNMENT = new CssMetaData("-fx-alignment", new EnumConverter(Pos.class), Pos.TOP_LEFT) { @Override public boolean isSettable(TilePane node) { return node.alignment == null || !node.alignment.isBound(); } @Override public StyleableProperty getStyleableProperty(TilePane node) { return (StyleableProperty)node.alignmentProperty(); } }; private static final CssMetaData PREF_COLUMNS = new CssMetaData("-fx-pref-columns", SizeConverter.getInstance(), 5.0) { @Override public boolean isSettable(TilePane node) { return node.prefColumns == null || !node.prefColumns.isBound(); } @Override public StyleableProperty getStyleableProperty(TilePane node) { return (StyleableProperty)node.prefColumnsProperty(); } }; private static final CssMetaData HGAP = new CssMetaData("-fx-hgap", SizeConverter.getInstance(), 0.0) { @Override public boolean isSettable(TilePane node) { return node.hgap == null || !node.hgap.isBound(); } @Override public StyleableProperty getStyleableProperty(TilePane node) { return (StyleableProperty)node.hgapProperty(); } }; private static final CssMetaData PREF_ROWS = new CssMetaData("-fx-pref-rows", SizeConverter.getInstance(), 5.0) { @Override public boolean isSettable(TilePane node) { return node.prefRows == null || !node.prefRows.isBound(); } @Override public StyleableProperty getStyleableProperty(TilePane node) { return (StyleableProperty)node.prefRowsProperty(); } }; private static final CssMetaData TILE_ALIGNMENT = new CssMetaData("-fx-tile-alignment", new EnumConverter(Pos.class), Pos.CENTER) { @Override public boolean isSettable(TilePane node) { return node.tileAlignment == null || !node.tileAlignment.isBound(); } @Override public StyleableProperty getStyleableProperty(TilePane node) { return (StyleableProperty)node.tileAlignmentProperty(); } }; private static final CssMetaData PREF_TILE_WIDTH = new CssMetaData("-fx-pref-tile-width", SizeConverter.getInstance(), USE_COMPUTED_SIZE) { @Override public boolean isSettable(TilePane node) { return node.prefTileWidth == null || !node.prefTileWidth.isBound(); } @Override public StyleableProperty getStyleableProperty(TilePane node) { return (StyleableProperty)node.prefTileWidthProperty(); } }; private static final CssMetaData PREF_TILE_HEIGHT = new CssMetaData("-fx-pref-tile-height", SizeConverter.getInstance(), USE_COMPUTED_SIZE) { @Override public boolean isSettable(TilePane node) { return node.prefTileHeight == null || !node.prefTileHeight.isBound(); } @Override public StyleableProperty getStyleableProperty(TilePane node) { return (StyleableProperty)node.prefTileHeightProperty(); } }; private static final CssMetaData ORIENTATION = new CssMetaData("-fx-orientation", new EnumConverter(Orientation.class), Orientation.HORIZONTAL) { @Override public Orientation getInitialValue(TilePane node) { // A vertical TilePane should remain vertical return node.getOrientation(); } @Override public boolean isSettable(TilePane node) { return node.orientation == null || !node.orientation.isBound(); } @Override public StyleableProperty getStyleableProperty(TilePane node) { return (StyleableProperty)node.orientationProperty(); } }; private static final CssMetaData VGAP = new CssMetaData("-fx-vgap", SizeConverter.getInstance(), 0.0) { @Override public boolean isSettable(TilePane node) { return node.vgap == null || !node.vgap.isBound(); } @Override public StyleableProperty getStyleableProperty(TilePane node) { return (StyleableProperty)node.vgapProperty(); } }; private static final List> STYLEABLES; static { final List> styleables = new ArrayList>(Region.getClassCssMetaData()); styleables.add(ALIGNMENT); styleables.add(HGAP); styleables.add(ORIENTATION); styleables.add(PREF_COLUMNS); styleables.add(PREF_ROWS); styleables.add(PREF_TILE_WIDTH); styleables.add(PREF_TILE_HEIGHT); styleables.add(TILE_ALIGNMENT); styleables.add(VGAP); STYLEABLES = Collections.unmodifiableList(styleables); } } /** * @return The CssMetaData associated with this class, which may include the * CssMetaData of its super classes. * @since JavaFX 8.0 */ public static List> getClassCssMetaData() { return StyleableProperties.STYLEABLES; } /** * {@inheritDoc} * * @since JavaFX 8.0 */ @Override public List> getCssMetaData() { return getClassCssMetaData(); } private abstract class TileSizeProperty extends ReadOnlyDoubleProperty { private final String name; private ExpressionHelper helper; private double value; private boolean valid; TileSizeProperty(String name, double initSize) { this.name = name; this.value = initSize; this.valid = initSize != -1; } @Override public Object getBean() { return TilePane.this; } @Override public String getName() { return name; } @Override public void addListener(InvalidationListener listener) { helper = ExpressionHelper.addListener(helper, this, listener); } @Override public void removeListener(InvalidationListener listener) { helper = ExpressionHelper.removeListener(helper, listener); } @Override public void addListener(ChangeListener listener) { helper = ExpressionHelper.addListener(helper, this, listener); } @Override public void removeListener(ChangeListener listener) { helper = ExpressionHelper.removeListener(helper, listener); } @Override public double get() { if (!valid) { value = compute(); valid = true; } return value; } public void invalidate() { if (valid) { valid = false; ExpressionHelper.fireValueChangedEvent(helper); } } public abstract double compute(); } }