javafx.scene.layout.TilePane Maven / Gradle / Ivy
/*
* Copyright (c) 2011, 2021, 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:
*
{@code
* 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:
*
{@code
* 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
* width height
* minimum
* left/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.
* preferred
* left/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.
* maximum
* Double.MAX_VALUE Double.MAX_VALUE
*
*
*
* Vertical
* width height
* minimum
* left/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.
* preferred
* left/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.
* maximum
* Double.MAX_VALUE Double.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 children's'
* 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.
*
*
*
* TilePane Constraint Table
* Constraint Type Description
* alignment javafx.geometry.Pos The alignment of the child within its tile.
* margin javafx.geometry.Insets Margin space around the outside of the child.
*
*
* Example:
*
{@code
* 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.
* @return the orientation of this tilepane
*/
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.
* @return the preferred number of rows 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.
* @return the preferred number of columns 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).
* @return the preferred width of each tile
*/
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).
* @return the preferred height of each tile
*/
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.
* @return the actual width of each tile
*/
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.
* @return the actual height of each tile
*/
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.
* @return 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.
* @return 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.
*
* @return the overall alignment of the tilepane's content within its width
* and height
*/
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.
* @return the default alignment of each child within its tile
*/
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.
*/
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 superclasses.
* @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 super Number> listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(ChangeListener super Number> 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();
}
}