
org.controlsfx.tools.Borders Maven / Gradle / Ivy
Show all versions of controlsfx Show documentation
/**
* Copyright (c) 2013, 2015, ControlsFX
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of ControlsFX, any associated website, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.controlsfx.tools;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javax.swing.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* A utility class that allows you to wrap JavaFX {@link Node Nodes} with a border,
* in a way somewhat analogous to the Swing {@link BorderFactory} (although with
* less options as a lot of what the Swing BorderFactory offers resulted in
* ugly borders!).
*
* The Borders class provides a fluent API for specifying the properties of
* each border. It is possible to create multiple borders around a Node simply
* by continuing to call additional methods before you call the final
* {@link Borders#build()} method. To use the Borders class, you simply call
* {@link Borders#wrap(Node)}, passing in the Node you wish to wrap the border(s)
* around.
*
*
Examples
* Firstly, lets wrap a JavaFX Button node with a simple line border that looks
* like the following:
*
*
*

*
* Here's the code:
*
*
* {@code
* Button button = new Button("Hello World!");
* Node wrappedButton = Borders.wrap(button).lineBorder().buildAll();
* }
*
* Easy, isn't it!? You can make the border look a little nicer by replacing
* the line border with an {@link EtchedBorders etched border}. An etched border
* has a subtle inner (or outer) line that makes the border stand out a bit more,
* like this:
*
*
*

*
* Now that's one good looking border! Here's the code:
*
*
* {@code
* Button button = new Button("Hello World!");
* Node wrappedButton = Borders.wrap(button).etchedBorder().buildAll();
* }
*
* In some circumstances you want to have multiple borders. For example,
* you might two line borders. That's easy:
*
*
*

*
*
* {@code
* Node wrappedButton = Borders.wrap(button)
* .lineBorder().color(Color.RED).build()
* .lineBorder().color(Color.GREEN).build()
* .build();
* }
*
* You simply chain the borders together, going from inside to outside!
*
* Because of all the configuration options it isn't possible to list all the
* functionality of all the border types, so refer to the rest of the javadocs
* for inspiration.
*/
public final class Borders {
/**************************************************************************
*
* Static fields
*
**************************************************************************/
private static final Color DEFAULT_BORDER_COLOR = Color.DARKGRAY;
/**************************************************************************
*
* Internal fields
*
**************************************************************************/
private final Node node;
private final List borders;
/**************************************************************************
*
* Fluent API entry method(s)
*
**************************************************************************/
public static Borders wrap(Node n) {
return new Borders(n);
}
/**************************************************************************
*
* Private Constructor
*
**************************************************************************/
private Borders(Node n) {
this.node = n;
this.borders = new ArrayList<>();
}
/**************************************************************************
*
* Fluent API
*
**************************************************************************/
/**
* Often times it is useful to have a bit of whitespace around a Node, to
* separate it from what it is next to. Call this method to begin building
* a border that will wrap the node with a given amount of whitespace
* (which can vary between the top, right, bottom, and left sides).
*/
public EmptyBorders emptyBorder() {
return new EmptyBorders(this);
}
/**
* The etched border look is essentially equivalent to the {@link #lineBorder()}
* look, except rather than one line, there are two. What is commonly done in
* this circumstance is that one of the lines is a very light colour (commonly
* white), which gives a nice etched look. Refer to the API in {@link EtchedBorders}
* for more information.
*/
public EtchedBorders etchedBorder() {
return new EtchedBorders(this);
}
/**
* Creates a nice, simple border around the node. Note that there are many
* configuration options in {@link LineBorders}, so explore it carefully.
*/
public LineBorders lineBorder() {
return new LineBorders(this);
}
/**
* Allows for developers to develop custom {@link Border} implementations,
* and to wrap them around a Node. Note that of course this is mostly
* redundant (as you could just call {@link Border#wrap(Node)} directly).
* The only benefit is if you're creating a compound border consisting of
* multiple borders, and you want your custom border included as part of
* this.
*/
public Borders addBorder(Border border) {
borders.add(border);
return this;
}
/**
* Returns the original node wrapped in zero or more borders, as specified
* using the fluent API.
*/
public Node build() {
// we iterate through the borders list in reverse order
Node bundle = node;
for (int i = borders.size() - 1; i >= 0; i--) {
Border border = borders.get(i);
bundle = border.wrap(bundle);
}
return bundle;
}
/**************************************************************************
*
* Support classes
*
**************************************************************************/
/**
* A fluent API that is only indirectly instantiable via the {@link Borders}
* fluent API, and which allows for an {@link Borders#emptyBorder() empty border}
* to be wrapped around a given Node.
*/
public class EmptyBorders {
private final Borders parent;
private double top;
private double right;
private double bottom;
private double left;
// private on purpose - this class is not directly instantiable.
private EmptyBorders(Borders parent) {
this.parent = parent;
}
/**
* Specifies that the wrapped Node should have the given padding around
* all four sides of itself.
*/
public EmptyBorders padding(double padding) {
return padding(padding, padding, padding, padding);
}
/**
* Specifies that the wrapped Node should be wrapped with the given
* padding for each of its four sides, going in the order top, right,
* bottom, and finally left.
*/
public EmptyBorders padding(double top, double right, double bottom, double left) {
this.top = top;
this.right = right;
this.bottom = bottom;
this.left = left;
return this;
}
/**
* Builds the {@link Border} and {@link Borders#addBorder(Border) adds it}
* to the list of borders to wrap around the given Node (which will be
* constructed and returned when {@link Borders#build()} is called.
*/
public Borders build() {
parent.addBorder(new StrokeBorder(null, buildStroke()));
return parent;
}
/**
* A convenience method, this is equivalent to calling
* {@link #build()} followed by {@link Borders#build()}. In other words,
* calling this will return the original Node wrapped in all its borders
* specified.
*/
public Node buildAll() {
build();
return parent.build();
}
private BorderStroke buildStroke() {
return new BorderStroke(
null,
BorderStrokeStyle.NONE,
null,
new BorderWidths(top, right, bottom, left),
Insets.EMPTY);
}
}
/**
* A fluent API that is only indirectly instantiable via the {@link Borders}
* fluent API, and which allows for an {@link Borders#etchedBorder() etched border}
* to be wrapped around a given Node.
*/
public class EtchedBorders {
private final Borders parent;
private String title;
private boolean raised = false;
private double outerTopPadding = 10;
private double outerRightPadding = 10;
private double outerBottomPadding = 10;
private double outerLeftPadding = 10;
private double innerTopPadding = 15;
private double innerRightPadding = 15;
private double innerBottomPadding = 15;
private double innerLeftPadding = 15;
private double topLeftRadius = 0;
private double topRightRadius = 0;
private double bottomRightRadius = 0;
private double bottomLeftRadius = 0;
private Color highlightColor = DEFAULT_BORDER_COLOR;
private Color shadowColor = Color.WHITE;
// private on purpose - this class is not directly instantiable.
private EtchedBorders(Borders parent) {
this.parent = parent;
}
/**
* Specifies the highlight colour to use in the etched border.
*/
public EtchedBorders highlight(Color highlight) {
this.highlightColor = highlight;
return this;
}
/**
* Specifies the shadow colour to use in the etched border.
*/
public EtchedBorders shadow(Color shadow) {
this.shadowColor = shadow;
return this;
}
/**
* Specifies the order in which the highlight and shadow colours are
* placed. A raised etched border has the shadow colour on the outside
* of the border, whereas a non-raised (or lowered) etched border has
* the shadow colour on the inside of the border.
*/
public EtchedBorders raised() {
raised = true;
return this;
}
/**
* If desired, this specifies the title text to show in this border.
*/
public EtchedBorders title(String title) {
this.title = title;
return this;
}
/**
* Specifies the outer padding of the four lines of this border.
*/
public EtchedBorders outerPadding(double padding) {
return outerPadding(padding, padding, padding, padding);
}
/**
* Specifies that the line wrapping the node should have outer padding
* as specified, with each padding being independently configured, going
* in the order top, right, bottom, and left.
*/
public EtchedBorders outerPadding(double topPadding, double rightPadding, double bottomPadding, double leftPadding) {
this.outerTopPadding = topPadding;
this.outerRightPadding = rightPadding;
this.outerBottomPadding = bottomPadding;
this.outerLeftPadding = leftPadding;
return this;
}
/**
* Specifies the inner padding of the four lines of this border.
*/
public EtchedBorders innerPadding(double padding) {
return innerPadding(padding, padding, padding, padding);
}
/**
* Specifies that the line wrapping the node should have inner padding
* as specified, with each padding being independently configured, going
* in the order top, right, bottom, and left.
*/
public EtchedBorders innerPadding(double topPadding, double rightPadding, double bottomPadding, double leftPadding) {
this.innerTopPadding = topPadding;
this.innerRightPadding = rightPadding;
this.innerBottomPadding = bottomPadding;
this.innerLeftPadding = leftPadding;
return this;
}
/**
* Specifies the radius of the four corners of the lines of this border.
*/
public EtchedBorders radius(double radius) {
return radius(radius, radius, radius, radius);
}
/**
* Specifies that the etched line wrapping the node should have corner radii
* as specified, with each radius being independently configured, going
* in the order top-left, top-right, bottom-right, and finally bottom-left.
*/
public EtchedBorders radius(double topLeft, double topRight, double bottomRight, double bottomLeft) {
this.topLeftRadius = topLeft;
this.topRightRadius = topRight;
this.bottomRightRadius = bottomRight;
this.bottomLeftRadius = bottomLeft;
return this;
}
/**
* Builds the {@link Border} and {@link Borders#addBorder(Border) adds it}
* to the list of borders to wrap around the given Node (which will be
* constructed and returned when {@link Borders#build()} is called.
*/
public Borders build() {
Color inner = raised ? shadowColor : highlightColor;
Color outer = raised ? highlightColor : shadowColor;
BorderStroke innerStroke = new BorderStroke(
inner,
BorderStrokeStyle.SOLID,
new CornerRadii(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, false),
new BorderWidths(1));
BorderStroke outerStroke = new BorderStroke(
outer,
BorderStrokeStyle.SOLID,
new CornerRadii(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, false),
new BorderWidths(1),
new Insets(1));
BorderStroke outerPadding = new EmptyBorders(parent)
.padding(outerTopPadding, outerRightPadding, outerBottomPadding, outerLeftPadding)
.buildStroke();
BorderStroke innerPadding = new EmptyBorders(parent)
.padding(innerTopPadding, innerRightPadding, innerBottomPadding, innerLeftPadding)
.buildStroke();
parent.addBorder(new StrokeBorder(null, outerPadding));
parent.addBorder(new StrokeBorder(title, innerStroke, outerStroke));
parent.addBorder(new StrokeBorder(null, innerPadding));
return parent;
}
/**
* A convenience method, this is equivalent to calling
* {@link #build()} followed by {@link Borders#build()}. In other words,
* calling this will return the original Node wrapped in all its borders
* specified.
*/
public Node buildAll() {
build();
return parent.build();
}
}
/**
* A fluent API that is only indirectly instantiable via the {@link Borders}
* fluent API, and which allows for a {@link Borders#lineBorder() line border}
* to be wrapped around a given Node.
*/
public class LineBorders {
private final Borders parent;
private String title;
private BorderStrokeStyle strokeStyle = BorderStrokeStyle.SOLID;
private Color topColor = DEFAULT_BORDER_COLOR;
private Color rightColor = DEFAULT_BORDER_COLOR;
private Color bottomColor = DEFAULT_BORDER_COLOR;
private Color leftColor = DEFAULT_BORDER_COLOR;
private double outerTopPadding = 10;
private double outerRightPadding = 10;
private double outerBottomPadding = 10;
private double outerLeftPadding = 10;
private double innerTopPadding = 15;
private double innerRightPadding = 15;
private double innerBottomPadding = 15;
private double innerLeftPadding = 15;
private double topThickness = 1;
private double rightThickness = 1;
private double bottomThickness = 1;
private double leftThickness = 1;
private double topLeftRadius = 0;
private double topRightRadius = 0;
private double bottomRightRadius = 0;
private double bottomLeftRadius = 0;
// private on purpose - this class is not directly instantiable.
private LineBorders(Borders parent) {
this.parent = parent;
}
/**
* Specifies the colour to use for all four sides of this border.
*/
public LineBorders color(Color color) {
return color(color, color, color, color);
}
/**
* Specifies that the wrapped Node should be wrapped with the given
* colours for each of its four sides, going in the order top, right,
* bottom, and finally left.
*/
public LineBorders color(Color topColor, Color rightColor, Color bottomColor, Color leftColor) {
this.topColor = topColor;
this.rightColor = rightColor;
this.bottomColor = bottomColor;
this.leftColor = leftColor;
return this;
}
/**
* Specifies which {@link BorderStrokeStyle} to use for this line border.
* By default this is {@link BorderStrokeStyle#SOLID}, but you can use
* any other style (such as {@link BorderStrokeStyle#DASHED},
* {@link BorderStrokeStyle#DOTTED}, or a custom style built using
* {@link BorderStrokeStyle#BorderStrokeStyle(javafx.scene.shape.StrokeType, javafx.scene.shape.StrokeLineJoin, javafx.scene.shape.StrokeLineCap, double, double, List)}.
*/
public LineBorders strokeStyle(BorderStrokeStyle strokeStyle) {
this.strokeStyle = strokeStyle;
return this;
}
/**
* Specifies the inner padding of the four lines of this border.
*/
public LineBorders outerPadding(double padding) {
return outerPadding(padding, padding, padding, padding);
}
/**
* Specifies that the line wrapping the node should have outer padding
* as specified, with each padding being independently configured, going
* in the order top, right, bottom, and left.
*/
public LineBorders outerPadding(double topPadding, double rightPadding, double bottomPadding, double leftPadding) {
this.outerTopPadding = topPadding;
this.outerRightPadding = rightPadding;
this.outerBottomPadding = bottomPadding;
this.outerLeftPadding = leftPadding;
return this;
}
/**
* Specifies the outer padding of the four lines of this border.
*/
public LineBorders innerPadding(double padding) {
return innerPadding(padding, padding, padding, padding);
}
/**
* Specifies that the line wrapping the node should have inner padding
* as specified, with each padding being independently configured, going
* in the order top, right, bottom, and left.
*/
public LineBorders innerPadding(double topPadding, double rightPadding, double bottomPadding, double leftPadding) {
this.innerTopPadding = topPadding;
this.innerRightPadding = rightPadding;
this.innerBottomPadding = bottomPadding;
this.innerLeftPadding = leftPadding;
return this;
}
/**
* Specifies the thickness of the line to use on all four sides of this
* border.
*/
public LineBorders thickness(double thickness) {
return thickness(thickness, thickness, thickness, thickness);
}
/**
* Specifies that the wrapped Node should be wrapped with the given
* line thickness for each of its four sides, going in the order top, right,
* bottom, and finally left.
*/
public LineBorders thickness(double topThickness, double rightThickness, double bottomThickness, double leftThickness) {
this.topThickness = topThickness;
this.rightThickness = rightThickness;
this.bottomThickness = bottomThickness;
this.leftThickness = leftThickness;
return this;
}
/**
* Specifies the radius of the four corners of the line of this border.
*/
public LineBorders radius(double radius) {
return radius(radius, radius, radius, radius);
}
/**
* Specifies that the line wrapping the node should have corner radii
* as specified, with each radius being independently configured, going
* in the order top-left, top-right, bottom-right, and finally bottom-left.
*/
public LineBorders radius(double topLeft, double topRight, double bottomRight, double bottomLeft) {
this.topLeftRadius = topLeft;
this.topRightRadius = topRight;
this.bottomRightRadius = bottomRight;
this.bottomLeftRadius = bottomLeft;
return this;
}
/**
* If desired, this specifies the title text to show in this border.
*/
public LineBorders title(String title) {
this.title = title;
return this;
}
/**
* Builds the {@link Border} and {@link Borders#addBorder(Border) adds it}
* to the list of borders to wrap around the given Node (which will be
* constructed and returned when {@link Borders#build()} is called.
*/
public Borders build() {
BorderStroke borderStroke = new BorderStroke(
topColor, rightColor, bottomColor, leftColor,
strokeStyle, strokeStyle, strokeStyle, strokeStyle,
new CornerRadii(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, false),
new BorderWidths(topThickness, rightThickness, bottomThickness, leftThickness),
null);
BorderStroke outerPadding = new EmptyBorders(parent)
.padding(outerTopPadding, outerRightPadding, outerBottomPadding, outerLeftPadding)
.buildStroke();
BorderStroke innerPadding = new EmptyBorders(parent)
.padding(innerTopPadding, innerRightPadding, innerBottomPadding, innerLeftPadding)
.buildStroke();
parent.addBorder(new StrokeBorder(null, outerPadding));
parent.addBorder(new StrokeBorder(title, borderStroke));
parent.addBorder(new StrokeBorder(null, innerPadding));
return parent;
}
/**
* A convenience method, this is equivalent to calling
* {@link #build()} followed by {@link Borders#build()}. In other words,
* calling this will return the original Node wrapped in all its borders
* specified.
*/
public Node buildAll() {
build();
return parent.build();
}
}
/**************************************************************************
*
* Support interfaces
*
**************************************************************************/
/**
* The public interface used by the {@link Borders} API to wrap nodes with
* zero or more Border implementations. ControlsFX ships with a few
* Border implementations (current {@link EmptyBorders}, {@link LineBorders},
* and {@link EtchedBorders}). As noted in {@link Borders#addBorder(Border)},
* this interface is relatively pointless, unless you plan to wrap a node
* with multiple borders and you want to use a custom {@link Border}
* implementation for at least one border. In this case, you can simply
* call {@link Borders#addBorder(Border)} with your custom border, when
* appropriate.
*/
@FunctionalInterface
public static interface Border {
/**
* Given a {@link Node}, this method should return a Node that contains
* the original Node and also has wrapped it with an appropriate border.
*/
public Node wrap(Node n);
}
/**************************************************************************
*
* Private support classes
*
**************************************************************************/
// --- Border implementations
private static class StrokeBorder implements Border {
private static final int TITLE_PADDING = 3;
private static final double GAP_PADDING = TITLE_PADDING * 2 - 1;
private final String title;
private final BorderStroke[] borderStrokes;
public StrokeBorder(String title, BorderStroke... borderStrokes) {
this.title = title;
this.borderStrokes = borderStrokes;
}
@Override public Node wrap(final Node n) {
StackPane pane = new StackPane() {
Label titleLabel;
{
// add in the node we are wrapping
getChildren().add(n);
// if the title string is set, then also add in the title label
if (title != null) {
titleLabel = new Label(title);
// give the text a bit of space on the left...
titleLabel.setPadding(new Insets(0, 0, 0, TITLE_PADDING));
getChildren().add(titleLabel);
}
}
@Override protected void layoutChildren() {
super.layoutChildren();
if (titleLabel != null) {
// layout the title label
final double labelHeight = titleLabel.prefHeight(-1);
final double labelWidth = titleLabel.prefWidth(labelHeight) + TITLE_PADDING;
titleLabel.resize(labelWidth, labelHeight);
titleLabel.relocate(TITLE_PADDING * 2, -labelHeight / 2.0 - 1);
List newBorderStrokes = new ArrayList<>(2);
// create a line gap for the title label
for (BorderStroke bs : borderStrokes) {
List dashList = new ArrayList<>();
// Create a dash list for the line gap or add it at the beginning of an existing dash list. This gap should be wide enough for the title label.
if (bs.getTopStyle().getDashArray().isEmpty())
dashList.addAll(Arrays.asList(GAP_PADDING, labelWidth, Double.MAX_VALUE));
else { // dash pattern exists
// insert gap in existing dash pattern and multiply original pattern so that gap does not show more then once
double origDashWidth = bs.getTopStyle().getDashArray().stream().mapToDouble(d -> d).sum();
if (origDashWidth > GAP_PADDING) {
dashList.add(GAP_PADDING);
dashList.add(labelWidth);
} else { // need to insert dash pattern before the gap
int no = (int) (GAP_PADDING / origDashWidth);
for (int i = 0; i < no; i++)
dashList.addAll(bs.getTopStyle().getDashArray());
if ((dashList.size() & 1) == 0) // if size is even number, add one more element because gap must be at odd index to be transparent
dashList.add(0d);
dashList.add(labelWidth + GAP_PADDING - no * origDashWidth);
}
for (int i = 0; i < (getWidth() - labelWidth - origDashWidth) / origDashWidth; i++)
dashList.addAll(bs.getTopStyle().getDashArray());
}
// create new border stroke style for the top border line with new dash list
BorderStrokeStyle topStrokeStyle = new BorderStrokeStyle(
bs.getTopStyle().getType(), bs.getTopStyle().getLineJoin(), bs.getTopStyle().getLineCap(),
bs.getTopStyle().getMiterLimit(), bs.getTopStyle().getDashOffset(), dashList);
// change existing border stroke to utilize new top border line stroke style
newBorderStrokes.add(new BorderStroke(
bs.getTopStroke(), bs.getRightStroke(), bs.getBottomStroke(), bs.getLeftStroke(),
topStrokeStyle, bs.getRightStyle(), bs.getBottomStyle(), bs.getLeftStyle(),
bs.getRadii(), bs.getWidths(), null));
}
setBorder(new javafx.scene.layout.Border(newBorderStrokes.toArray(new BorderStroke[newBorderStrokes.size()])));
}
}
};
pane.setBorder(new javafx.scene.layout.Border(borderStrokes));
return pane;
}
}
}