All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.controlsfx.tools.Borders Maven / Gradle / Ivy

Go to download

High quality UI controls and other tools to complement the core JavaFX distribution

There is a newer version: 11.2.1
Show newest version
/**
 * 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: * *
*

Screenshot of Borders.LineBorders
* *

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: * *
*

Screenshot of Borders.EtchedBorders
* *

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: * *
*

Screenshot of two Borders.LineBorders
* *
 * {@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; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy