javafx.scene.layout.FlowPane 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 java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.DoublePropertyBase;
import javafx.beans.property.ObjectProperty;
import javafx.css.CssMetaData;
import javafx.css.StyleableDoubleProperty;
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.css.Styleable;
import static javafx.geometry.Orientation.*;
import javafx.util.Callback;
/**
* FlowPane lays out its children in a flow that wraps at the flowpane's boundary.
*
* A horizontal flowpane (the default) will layout nodes in rows, wrapping at the
* flowpane's width. A vertical flowpane lays out nodes in columns,
* wrapping at the flowpane's height. If the flowpane has a border and/or padding set,
* the content will be flowed within those insets.
*
* FlowPane's prefWrapLength property establishes its preferred width
* (for horizontal) or preferred height (for vertical). Applications should set
* prefWrapLength if the default value (400) doesn't suffice. Note that prefWrapLength
* is used only for calculating the preferred size and may not reflect the actual
* wrapping dimension, which tracks the actual size of the flowpane.
*
* The alignment property controls how the rows and columns are aligned
* within the bounds of the flowpane and defaults to Pos.TOP_LEFT. It is also possible
* to control the alignment of nodes within the rows and columns by setting
* rowValignment for horizontal or columnHalignment for vertical.
*
* Example of a horizontal flowpane:
*
{@code
* Image images[] = { ... };
* FlowPane flow = new FlowPane();
* flow.setVgap(8);
* flow.setHgap(4);
* flow.setPrefWrapLength(300); // preferred width = 300
* for (int i = 0; i < images.length; i++) {
* flow.getChildren().add(new ImageView(image[i]);
* }
* }
*
*
* Example of a vertical flowpane:
*
{@code
* FlowPane flow = new FlowPane(Orientation.VERTICAL);
* flow.setColumnHalignment(HPos.LEFT); // align labels on left
* flow.setPrefWrapLength(200); // preferred height = 200
* for (int i = 0; i < titles.size(); i++) {
* flow.getChildren().add(new Label(titles[i]);
* }
* }
*
*
* FlowPane lays out each managed child regardless of the child's visible property value;
* unmanaged children are ignored for all layout calculations.
*
*
* FlowPane may be styled with backgrounds and borders using CSS. See
* {@link javafx.scene.layout.Region Region} superclass for details.
*
* Resizable Range
*
*
* A flowpane's parent will resize the flowpane within the flowpane's resizable range
* during layout. By default the flowpane computes this range based on its content
* as outlined in the tables below.
*
*
* Horizontal
* width height
* minimum
* left/right insets plus largest of children's pref widths
* top/bottom insets plus height required to display all children at their preferred heights when wrapped at a specified width
* preferred
* left/right insets plus prefWrapLength
* top/bottom insets plus height required to display all children at their pref heights when wrapped at a specified width
* maximum
* Double.MAX_VALUE Double.MAX_VALUE
*
*
*
* Vertical
* width height
* minimum
* left/right insets plus width required to display all children at their preferred widths when wrapped at a specified height
* top/bottom insets plus largest of children's pref heights
* preferred
* left/right insets plus width required to display all children at their pref widths when wrapped at the specified height
* top/bottom insets plus prefWrapLength
* maximum
* Double.MAX_VALUE Double.MAX_VALUE
*
*
* A flowpane'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.
*
* FlowPane 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:
*
* flowPane.setMaxWidth(500);
*
* Applications may restore the computed values by setting these properties back
* to Region.USE_COMPUTED_SIZE.
*
* FlowPane does not clip its content by default, so it is possible that children's
* bounds may extend outside its own bounds if a child's pref size is larger than
* the space flowpane has to allocate for it.
*
* @since JavaFX 2.0
*/
public class FlowPane extends Pane {
/* ******************************************************************
* BEGIN static methods
********************************************************************/
private static final String MARGIN_CONSTRAINT = "flowpane-margin";
/**
* Sets the margin for the child when contained by a flowpane.
* If set, the flowpane will layout it out with the margin space around it.
* Setting the value to null will remove the constraint.
* @param child the child node of a flowpane
* @param value the margin of space around the child
*/
public static void setMargin(Node child, Insets value) {
setConstraint(child, MARGIN_CONSTRAINT, value);
}
/**
* Returns the child's margin constraint if set.
* @param child the child node of a flowpane
* @return the margin for the child or null if no margin was set
*/
public static Insets getMargin(Node child) {
return (Insets)getConstraint(child, MARGIN_CONSTRAINT);
}
private static final Callback marginAccessor = n -> getMargin(n);
/**
* Removes all flowpane constraints from the child node.
* @param child the child node
*/
public static void clearConstraints(Node child) {
setMargin(child, null);
}
/* ******************************************************************
* END static methods
********************************************************************/
/**
* Creates a horizontal FlowPane layout with hgap/vgap = 0.
*/
public FlowPane() {
super();
}
/**
* Creates a FlowPane layout with the specified orientation and hgap/vgap = 0.
* @param orientation the direction the tiles should flow & wrap
*/
public FlowPane(Orientation orientation) {
this();
setOrientation(orientation);
}
/**
* Creates a horizontal FlowPane layout with 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 FlowPane(double hgap, double vgap) {
this();
setHgap(hgap);
setVgap(vgap);
}
/**
* Creates a FlowPane layout with the specified orientation and hgap/vgap.
* @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 FlowPane(Orientation orientation, double hgap, double vgap) {
this();
setOrientation(orientation);
setHgap(hgap);
setVgap(vgap);
}
/**
* Creates a horizontal FlowPane layout with hgap/vgap = 0.
* @param children The initial set of children for this pane.
* @since JavaFX 8.0
*/
public FlowPane(Node... children) {
super();
getChildren().addAll(children);
}
/**
* Creates a FlowPane layout with the specified orientation 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 FlowPane(Orientation orientation, Node... children) {
this();
setOrientation(orientation);
getChildren().addAll(children);
}
/**
* Creates a horizontal FlowPane layout with 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 FlowPane(double hgap, double vgap, Node... children) {
this();
setHgap(hgap);
setVgap(vgap);
getChildren().addAll(children);
}
/**
* Creates a FlowPane layout with the specified orientation and hgap/vgap.
* @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 FlowPane(Orientation orientation, double hgap, double vgap, Node... children) {
this();
setOrientation(orientation);
setHgap(hgap);
setVgap(vgap);
getChildren().addAll(children);
}
/**
* The orientation of this flowpane.
* A horizontal flowpane lays out children left to right, wrapping at the
* flowpane's width boundary. A vertical flowpane lays out children top to
* bottom, wrapping at the flowpane's height.
* The default is horizontal.
* @return the orientation of this flowpane
*/
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 FlowPane.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 amount of horizontal space between each node in a horizontal flowpane
* or the space between columns in a vertical flowpane.
* @return the amount of horizontal space between each node in a horizontal
* flowpane or the space between columns in a vertical flowpane
*/
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 FlowPane.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 node in a vertical flowpane
* or the space between rows in a horizontal flowpane.
* @return the amount of vertical space between each node in a vertical
* flowpane or the space between rows in a horizontal flowpane
*/
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 FlowPane.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 preferred width where content should wrap in a horizontal flowpane or
* the preferred height where content should wrap in a vertical flowpane.
*
* This value is used only to compute the preferred size of the flowpane and may
* not reflect the actual width or height, which may change if the flowpane is
* resized to something other than its preferred size.
*
* Applications should initialize this value to define a reasonable span
* for wrapping the content.
*
* @return the preferred width where content should wrap in a horizontal
* flowpane or the preferred height where content should wrap in a vertical
* flowpane
*/
public final DoubleProperty prefWrapLengthProperty() {
if (prefWrapLength == null) {
prefWrapLength = new DoublePropertyBase(400) {
@Override
protected void invalidated() {
requestLayout();
}
@Override
public Object getBean() {
return FlowPane.this;
}
@Override
public String getName() {
return "prefWrapLength";
}
};
}
return prefWrapLength;
}
private DoubleProperty prefWrapLength;
public final void setPrefWrapLength(double value) { prefWrapLengthProperty().set(value); }
public final double getPrefWrapLength() { return prefWrapLength == null ? 400 : prefWrapLength.get(); }
/**
* The overall alignment of the flowpane's content within its width and height.
*
For a horizontal flowpane, each row will be aligned within the flowpane's width
* using the alignment's hpos value, and the rows will be aligned within the
* flowpane's height using the alignment's vpos value.
*
For a vertical flowpane, each column will be aligned within the flowpane's height
* using the alignment's vpos value, and the columns will be aligned within the
* flowpane's width using the alignment's hpos value.
* @return the overall alignment of the flowpane'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 FlowPane.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 horizontal alignment of nodes within each column of a vertical flowpane.
* The property is ignored for horizontal flowpanes.
* @return the horizontal alignment of nodes within each column of a
* vertical flowpane
*/
public final ObjectProperty columnHalignmentProperty() {
if (columnHalignment == null) {
columnHalignment = new StyleableObjectProperty(HPos.LEFT) {
@Override
public void invalidated() {
requestLayout();
}
@Override
public CssMetaData getCssMetaData() {
return StyleableProperties.COLUMN_HALIGNMENT;
}
@Override
public Object getBean() {
return FlowPane.this;
}
@Override
public String getName() {
return "columnHalignment";
}
};
}
return columnHalignment;
}
private ObjectProperty columnHalignment;
public final void setColumnHalignment(HPos value) { columnHalignmentProperty().set(value); }
public final HPos getColumnHalignment() { return columnHalignment == null ? HPos.LEFT : columnHalignment.get(); }
private HPos getColumnHalignmentInternal() {
HPos localPos = getColumnHalignment();
return localPos == null ? HPos.LEFT : localPos;
}
/**
* The vertical alignment of nodes within each row of a horizontal flowpane.
* If this property is set to VPos.BASELINE, then the flowpane will always
* resize children to their preferred heights, rather than expanding heights
* to fill the row height.
* The property is ignored for vertical flowpanes.
* @return the vertical alignment of nodes within each row of a horizontal
* flowpane
*/
public final ObjectProperty rowValignmentProperty() {
if (rowValignment == null) {
rowValignment = new StyleableObjectProperty(VPos.CENTER) {
@Override
public void invalidated() {
requestLayout();
}
@Override
public CssMetaData getCssMetaData() {
return StyleableProperties.ROW_VALIGNMENT;
}
@Override
public Object getBean() {
return FlowPane.this;
}
@Override
public String getName() {
return "rowValignment";
}
};
}
return rowValignment;
}
private ObjectProperty rowValignment;
public final void setRowValignment(VPos value) { rowValignmentProperty().set(value); }
public final VPos getRowValignment() { return rowValignment == null ? VPos.CENTER : rowValignment.get(); }
private VPos getRowValignmentInternal() {
VPos localPos = getRowValignment();
return localPos == null ? VPos.CENTER : localPos;
}
@Override public Orientation getContentBias() {
return getOrientation();
}
@Override protected double computeMinWidth(double height) {
if (getContentBias() == HORIZONTAL) {
double maxPref = 0;
final List children = getChildren();
for (int i=0, size=children.size(); i children = getChildren();
for (int i=0, size=children.size(); i hruns = getRuns(maxRunWidth);
double w = computeContentWidth(hruns);
w = getPrefWrapLength() > w ? getPrefWrapLength() : w;
return insets.getLeft() + snapSizeX(w) + insets.getRight();
} else {
// vertical
double maxRunHeight = forHeight != -1?
forHeight - insets.getTop() - insets.getBottom() : getPrefWrapLength();
List vruns = getRuns(maxRunHeight);
return insets.getLeft() + computeContentWidth(vruns) + insets.getRight();
}
}
@Override protected double computePrefHeight(double forWidth) {
final Insets insets = getInsets();
if (getOrientation() == HORIZONTAL) {
// horizontal
double maxRunWidth = forWidth != -1?
forWidth - insets.getLeft() - insets.getRight() : getPrefWrapLength();
List hruns = getRuns(maxRunWidth);
return insets.getTop() + computeContentHeight(hruns) + insets.getBottom();
} else {
// vertical
double maxRunHeight = getPrefWrapLength();
List vruns = getRuns(maxRunHeight);
double h = computeContentHeight(vruns);
h = getPrefWrapLength() > h ? getPrefWrapLength() : h;
return insets.getTop() + snapSizeY(h) + insets.getBottom();
}
}
@Override public void requestLayout() {
if (!computingRuns) {
runs = null;
}
super.requestLayout();
}
private List runs = null;
private double lastMaxRunLength = -1;
boolean computingRuns = false;
private List getRuns(double maxRunLength) {
if (runs == null || maxRunLength != lastMaxRunLength) {
computingRuns = true;
lastMaxRunLength = maxRunLength;
runs = new ArrayList();
double runLength = 0;
double runOffset = 0;
Run run = new Run();
double vgap = snapSpaceY(this.getVgap());
double hgap = snapSpaceX(this.getHgap());
final List children = getChildren();
for (int i=0, size=children.size(); i maxRunLength && runLength > 0) {
// wrap to next run *unless* its the only node in the run
normalizeRun(run, runOffset);
if (getOrientation() == HORIZONTAL) {
// horizontal
runOffset += run.height + vgap;
} else {
// vertical
runOffset += run.width + hgap;
}
runs.add(run);
runLength = 0;
run = new Run();
}
if (getOrientation() == HORIZONTAL) {
// horizontal
nodeRect.x = runLength;
runLength += nodeRect.width + hgap;
} else {
// vertical
nodeRect.y = runLength;
runLength += nodeRect.height + vgap;
}
run.rects.add(nodeRect);
}
}
// insert last run
normalizeRun(run, runOffset);
runs.add(run);
computingRuns = false;
}
return runs;
}
private void normalizeRun(final Run run, double runOffset) {
if (getOrientation() == HORIZONTAL) {
// horizontal
ArrayList rownodes = new ArrayList();
run.width = (run.rects.size()-1)*snapSpaceX(getHgap());
for (int i=0, max=run.rects.size(); i run.rects.get(i).width, run.height, true) : 0;
} else {
// vertical
run.height = (run.rects.size()-1)*snapSpaceY(getVgap());
double maxw = 0;
for (int i=0, max=run.rects.size(); i runs) {
double cwidth = getOrientation() == HORIZONTAL ? 0 : (runs.size()-1)*snapSpaceX(getHgap());
for (int i=0, max=runs.size(); i runs) {
double cheight = getOrientation() == VERTICAL ? 0 : (runs.size()-1)*snapSpaceY(getVgap());
for (int i=0, max=runs.size(); i runs = getRuns(getOrientation() == HORIZONTAL ? insideWidth : insideHeight);
// Now that the nodes are broken into runs, figure out alignments
for (int i=0, max=runs.size(); i(Pos.class), Pos.TOP_LEFT) {
@Override
public boolean isSettable(FlowPane node) {
return node.alignment == null || !node.alignment.isBound();
}
@Override
public StyleableProperty getStyleableProperty(FlowPane node) {
return (StyleableProperty)node.alignmentProperty();
}
};
private static final CssMetaData COLUMN_HALIGNMENT =
new CssMetaData("-fx-column-halignment",
new EnumConverter(HPos.class), HPos.LEFT) {
@Override
public boolean isSettable(FlowPane node) {
return node.columnHalignment == null || !node.columnHalignment.isBound();
}
@Override
public StyleableProperty getStyleableProperty(FlowPane node) {
return (StyleableProperty)node.columnHalignmentProperty();
}
};
private static final CssMetaData HGAP =
new CssMetaData("-fx-hgap",
SizeConverter.getInstance(), 0.0){
@Override
public boolean isSettable(FlowPane node) {
return node.hgap == null || !node.hgap.isBound();
}
@Override
public StyleableProperty getStyleableProperty(FlowPane node) {
return (StyleableProperty)node.hgapProperty();
}
};
private static final CssMetaData ROW_VALIGNMENT =
new CssMetaData("-fx-row-valignment",
new EnumConverter(VPos.class), VPos.CENTER) {
@Override
public boolean isSettable(FlowPane node) {
return node.rowValignment == null || !node.rowValignment.isBound();
}
@Override
public StyleableProperty getStyleableProperty(FlowPane node) {
return (StyleableProperty)node.rowValignmentProperty();
}
};
private static final CssMetaData ORIENTATION =
new CssMetaData("-fx-orientation",
new EnumConverter(Orientation.class),
Orientation.HORIZONTAL) {
@Override
public Orientation getInitialValue(FlowPane node) {
// A vertical flow pane should remain vertical
return node.getOrientation();
}
@Override
public boolean isSettable(FlowPane node) {
return node.orientation == null || !node.orientation.isBound();
}
@Override
public StyleableProperty getStyleableProperty(FlowPane node) {
return (StyleableProperty)node.orientationProperty();
}
};
private static final CssMetaData VGAP =
new CssMetaData("-fx-vgap",
SizeConverter.getInstance(), 0.0){
@Override
public boolean isSettable(FlowPane node) {
return node.vgap == null || !node.vgap.isBound();
}
@Override
public StyleableProperty getStyleableProperty(FlowPane node) {
return (StyleableProperty)node.vgapProperty();
}
};
private static final List> STYLEABLES;
static {
final List> styleables =
new ArrayList>(Region.getClassCssMetaData());
styleables.add(ALIGNMENT);
styleables.add(COLUMN_HALIGNMENT);
styleables.add(HGAP);
styleables.add(ROW_VALIGNMENT);
styleables.add(ORIENTATION);
styleables.add(VGAP);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
/**
* Gets the {@code CssMetaData} associated with this class, which may include the
* {@code CssMetaData} of its superclasses.
* @return the {@code CssMetaData}
* @since JavaFX 8.0
*/
public static List> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
/**
* {@inheritDoc}
*
* @since JavaFX 8.0
*/
@Override
public List> getCssMetaData() {
return getClassCssMetaData();
}
//REMIND(aim); replace when we get mutable rects
private static class LayoutRect {
public Node node;
double x;
double y;
double width;
double height;
@Override public String toString() {
return "LayoutRect node id="+node.getId()+" "+x+","+y+" "+width+"x"+height;
}
}
private static class Run {
ArrayList rects = new ArrayList();
double width;
double height;
double baselineOffset;
}
}