/*
* Copyright (c) 2002-2015, JIDE Software Inc. 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.
*/
package jidefx.scene.control.decoration;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.layout.Region;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
/**
* {@code DecorationUtils} provides a few convenient ways to install (and uninstall) decorators to a target node.
* <p/>
* You can install multiple decorators to the same node, which can be achieved by calling install several times or
* installAll with all the decorators. However if you only call install or installAll methods, the decorations won't
* show up yet. The other part of the story is to use DecorationPane as the targetNode?s ancestor. It doesn't have to be
* the immediate ancestor. It will work as long as the DecorationPane is one of its ancestors. You can create a form
* with a bunch of nodes, fields, comboboxes, tables, lists, whatever you want, call DecorationUtils.install to add
* decoration to some of them, then wrap the whole form in the DecorationPane at the end. See below for a sample code.
* <p/>
* <pre>{@code
* // create nodes and add it to a pane
* Pane pane = new Xxxx ();
* pane.getChildren().addAll(?);
* return new DecorationPane(pane); // instead of return pane, you wrap it
* into
* a DecorationPane
* }</pre>
* <p/>
* It is the DecorationPane which will search for all the decorators installed on its children (more precisely,
* descendants) and placed them at the position as specified in the Decorator interface.
* <p/>
* Please note, JavaFX only allows a Node to be added to the same Scene just once. The decoration is a Node as well so
* it cannot be reused to decorate several target Nodes. If you want to use the same decoration several times, please
* use a factory pattern like this.
* <p/>
* <pre>{@code
* Supplier<Decorator> asteriskFactory = new Supplier<Decorator>() {
* public Decorator get() {
* Label label = new Label("*", asteriskImage);
* return new Decorator(label, Pos.TOP_RIGHT);
* }
* };
* // you can use it's create method to create multiple same decorators and use
* them on different Nodes.
* DecorationUtils.install(nameField, asteriskFactory.get());
* DecorationUtils.install(emailField, asteriskFactory.get());
* }</pre>
*/
@SuppressWarnings("UnusedDeclaration")
public class DecorationUtils {
/**
* The property name where the decorators are installed on a Node's getProperties().
*/
private static final String PROPERTY_DECORATOR = "Decoration.Decorator"; //NON-NLS
/**
* The property name where the old insets is saved on a Node's getProperties().
*/
private static final String PROPERTY_TARGET_NODE_PADDING = "Decoration.Target.Node.Padding"; //NON-NLS
/**
* The property name on a target node which indicates the status of all decoration nodes
*/
private static final String PROPERTY_TARGET_DECORATOR_STATUS = "Decoration.Target.Decorator.Status"; //NON-NLS
/**
* Indicates whether decorate node has been removed from decoration pane from outside. For example, decoration node
* will bee cleaned from table column header by sorting.
*/
private static final String PROPERTY_TARGET_DECORATOR_STATUS_CLEANED = "cleaned"; //NON-NLS
/**
* The property name on a decorate node which indicates the status of the animation
*/
private static final String PROPERTY_DECORATOR_ANIMATION_STATUS = "Decoration.Decorator.Animation.Status"; //NON-NLS
/**
* Indicates whether the animation on decorate node has been played.
*/
private static final String PROPERTY_DECORATOR_ANIMATION_STATUS_PLAYED = "played"; //NON-NLS
/**
* Installs a {@code Decorator} to a target Node.
*
* @param targetNode the target Node where the {@code Decorator} will be installed.
* @param decorator a {@code Decorator}
*/
public static void install(Node targetNode, Decorator decorator) {
if (targetNode != null && decorator != null) {
decorator.getNode().setVisible(false);
Object object = getDecorators(targetNode);
if (object instanceof Decorator) {
if (!object.equals(decorator)) {
installAll(targetNode, (Decorator) object, decorator);
}
}
else if (object instanceof Decorator[]) {
Decorator[] oldDecorators = (Decorator[]) object;
if (!Arrays.asList(oldDecorators).contains(decorator)) {
Decorator[] newDecorators = new Decorator[oldDecorators.length + 1];
System.arraycopy(oldDecorators, 0, newDecorators, 0, oldDecorators.length);
newDecorators[oldDecorators.length] = decorator;
targetNode.getProperties().put(PROPERTY_DECORATOR, newDecorators);
}
}
else {
targetNode.getProperties().put(PROPERTY_DECORATOR, decorator);
}
if (targetNode instanceof Parent){
((Parent) targetNode).requestLayout();
}
}
}
/**
* Installs some {@code Decorator}s to a target Node.
*
* @param targetNode the target Node where the {@code Decorator}s will be installed.
* @param decorators {@code Decorator}s that will be installed.
*/
public static void installAll(Node targetNode, Decorator... decorators) {
if (targetNode != null && decorators != null) {
for (Decorator decorator : decorators) {
if (decorator != null) {
decorator.getNode().setVisible(false);
}
}
targetNode.getProperties().put(PROPERTY_DECORATOR, decorators);
}
}
/**
* Installs a same {@code Decorator} to many target Nodes. Because each {@code Decorator} instance can only be
* installed to one Node, that's why we use a Factory here so that for each target Node, a new {@code Decorator}
* will be created.
*
* @param targetNodes the target Nodes where the {@code Decorator} will be installed.
* @param decoratorFactory a Factory that will create a new {@code Decorator} for each target Node.
*/
public static void installAll(List<Node> targetNodes, Supplier<Decorator> decoratorFactory) {
for (Node node : targetNodes) {
install(node, decoratorFactory.get());
}
}
/**
* Checks if the node has decorator(s) installed.
*
* @param targetNode the target Node.
* @return true if there are decorators, otherwise false.
*/
public static boolean hasDecorators(Node targetNode) {
Object object = targetNode.getProperties().get(DecorationUtils.PROPERTY_DECORATOR);
return object instanceof Decorator || object instanceof Decorator[];
}
/**
* Gets the decorators.
*
* @param targetNode the target Node.
* @return an object that could be a Decorator or a Decorator array.
*/
public static Object getDecorators(Node targetNode) {
return targetNode.getProperties().get(DecorationUtils.PROPERTY_DECORATOR);
}
/**
* Uninstalls all decorators from the target Node.
*
* @param targetNode the target Node.
*/
public static void uninstall(Node targetNode) {
if (targetNode != null) {
targetNode.getProperties().remove(PROPERTY_DECORATOR);
targetNode.getProperties().remove(DecorationUtils.PROPERTY_TARGET_DECORATOR_STATUS);
if (targetNode instanceof Parent){
((Parent) targetNode).requestLayout();
}
}
}
/**
* Uninstalls a decorator from the target Node.
*
* @param targetNode the target Node.
* @param decorator the decorator to be uninstalled.
*/
public static void uninstall(Node targetNode, Decorator decorator) {
if (targetNode != null && decorator != null) {
Object object = targetNode.getProperties().get(PROPERTY_DECORATOR);
if (object instanceof Decorator && object.equals(decorator)) {
targetNode.getProperties().remove(PROPERTY_DECORATOR);
}
else if (object instanceof Decorator[]) {
Decorator[] oldDecorators = (Decorator[]) object;
if (oldDecorators.length == 1) {
if (oldDecorators[0].equals(decorator)) {
targetNode.getProperties().remove(PROPERTY_DECORATOR);
}
}
else {
Decorator[] tempDecorators = new Decorator[oldDecorators.length];
int j = 0;
for (Decorator oldDecorator : oldDecorators) {
if (!oldDecorator.equals(decorator)) {
tempDecorators[j++] = oldDecorator;
}
}
if (j == 0) {
targetNode.getProperties().remove(PROPERTY_DECORATOR);
}
else if (j < oldDecorators.length) {
Decorator[] newDecorators = new Decorator[j];
System.arraycopy(tempDecorators, 0, newDecorators, 0, j);
targetNode.getProperties().put(PROPERTY_DECORATOR, newDecorators);
}
}
}
if (targetNode instanceof Parent){
((Parent) targetNode).requestLayout();
}
}
}
private static boolean isVisible(Node c) {
Node parent = c;
while (parent != null) {
if (!parent.isVisible()) {
return false;
}
parent = parent.getParent();
}
return true;
}
/**
* A util method that computes the bounds of the decorations.
*
* @param targetNode the target node.
* @param decorationNode the decoration node.
* @param decorator the decorator.
* @return the bounds of the decoration.
*/
private static Bounds computeDecorationBounds(Node targetNode, Node decorationNode, Decorator decorator) {
double width = decorationNode.prefWidth(-1);
double height = decorationNode.prefHeight(-1);
// we use local coordinate to do the calculation
// default TOP_LEFT
// Bounds targetBounds = targetNode.getBoundsInLocal(); // ensure position will be right after the transition is applied. it caused a shift of the decoration when the field gets focus.
Bounds targetBounds = targetNode.getLayoutBounds(); // this will probably not work when the node is in transition
// adjust to center of the decoration node
double x = targetBounds.getMinX() - width / 2;
double y = targetBounds.getMinY() - height / 2;
double targetWidth = targetBounds.getWidth();
double targetHeight = targetBounds.getHeight();
if (targetWidth <= 0) {
targetWidth = targetNode.prefWidth(-1);
}
if (targetHeight <= 0) {
targetHeight = targetNode.prefHeight(-1);
}
double baselineOffset = targetNode.getBaselineOffset();
Pos pos = decorator.getPos();
// position
switch (pos) {
case TOP_CENTER:
x += targetWidth / 2;
break;
case TOP_RIGHT:
x += targetWidth;
break;
case CENTER_LEFT:
y += targetHeight / 2;
break;
case CENTER:
x += targetWidth / 2;
y += targetHeight / 2;
break;
case CENTER_RIGHT:
x += targetWidth;
y += targetHeight / 2;
break;
case BOTTOM_LEFT:
y += targetHeight;
break;
case BOTTOM_CENTER:
x += targetWidth / 2;
y += targetHeight;
break;
case BOTTOM_RIGHT:
x += targetWidth;
y += targetHeight;
break;
case BASELINE_LEFT:
y += baselineOffset - decorationNode.getBaselineOffset() + height / 2;
break;
case BASELINE_CENTER:
x += targetWidth / 2;
y += baselineOffset - decorationNode.getBaselineOffset() + height / 2;
break;
case BASELINE_RIGHT:
x += targetWidth;
y += baselineOffset - decorationNode.getBaselineOffset() + height / 2;
break;
}
// offset
Point2D decoratorPosOffset = decorator.getPosOffset();
if (decorator.isValueInPercent()) {
x += decoratorPosOffset.getX() * width / 100;
y += decoratorPosOffset.getY() * height / 100;
}
else {
x += decoratorPosOffset.getX();
y += decoratorPosOffset.getY();
}
return adjustBounds(decorationNode.getLayoutBounds(), new BoundingBox(x, y, width, height));
}
/**
* A util method that computes the bounds of the decorations.
*
* @param parentNode the parent node.
* @param targetNode the target node.
* @param decorateNode the decoration node.
* @param decorator the decorator.
* @return the bounds of the decoration.
*/
static Bounds computeDecorationBounds(Node parentNode, Node targetNode, Node decorateNode, Decorator decorator) {
return parentNode.screenToLocal(targetNode.localToScreen(computeDecorationBounds(targetNode, decorateNode, decorator)));
}
/**
* To determine if the decorators on a node were cleaned.
*
* @param targetNode the node which has decorators.
* @return true, means the decorators on this node had been cleaned. false, means the decorators on this node were
* not cleaned.
* @see #setTargetDecoratorCleaned(javafx.scene.Node, boolean)
*/
static <T extends Node> boolean isTargetDecoratorCleaned(T targetNode) {
return targetNode != null && PROPERTY_TARGET_DECORATOR_STATUS_CLEANED.equals(targetNode.getProperties().get(PROPERTY_TARGET_DECORATOR_STATUS));
}
/**
* Set the status of decorators on a node. There are only two status cleaned, means the decorators on this node had
* been cleaned somehow. not cleaned.
*
* @param targetNode the node which has decorators.
* @param clean true, set the status to be status of cleaned. false, clear the status.
*/
public static void setTargetDecoratorCleaned(Node targetNode, boolean clean) {
if (targetNode == null) {
return;
}
if (clean) {
targetNode.getProperties().put(PROPERTY_TARGET_DECORATOR_STATUS, PROPERTY_TARGET_DECORATOR_STATUS_CLEANED);
}
else {
targetNode.getProperties().remove(PROPERTY_TARGET_DECORATOR_STATUS);
}
}
static Insets getTargetPadding(Region targetNode) {
if (targetNode == null) return null;
else {
Object insets = targetNode.getProperties().get(PROPERTY_TARGET_NODE_PADDING);
if (insets instanceof Insets) return (Insets) insets;
else {
Insets padding = targetNode.getPadding();
setTargetPadding(targetNode, padding);
return padding;
}
}
}
static void setTargetPadding(Region targetNode, Insets rawInsets) {
if (targetNode == null) {
return;
}
targetNode.getProperties().put(PROPERTY_TARGET_NODE_PADDING, rawInsets);
}
/**
* To determine if the animation on a decorator has been played.
*
* @param decorateNode the decorate node related with an animation.
* @return true, means the animation has been played. false, means the animation has not been played.
* @see #setAnimationPlayed(javafx.scene.Node, boolean)
*/
public static boolean isAnimationPlayed(Node decorateNode) {
if (decorateNode == null) {
return false;
}
Object status = decorateNode.getProperties().get(PROPERTY_DECORATOR_ANIMATION_STATUS);
return status != null && status.equals(PROPERTY_DECORATOR_ANIMATION_STATUS_PLAYED);
}
/**
* Set the status of playing of the animation on a decorator.
*
* @param decorateNode the decorate node related with an animation.
* @param played the status to be set.
* @see #isAnimationPlayed(javafx.scene.Node)
*/
public static void setAnimationPlayed(Node decorateNode, boolean played) {
if (decorateNode == null) {
return;
}
if (played) {
decorateNode.getProperties().put(PROPERTY_DECORATOR_ANIMATION_STATUS, PROPERTY_DECORATOR_ANIMATION_STATUS_PLAYED);
}
else {
decorateNode.getProperties().remove(PROPERTY_DECORATOR_ANIMATION_STATUS);
}
}
static <T extends Node> Insets computePadding(Region targetNode, Decorator<T> decorator) {
Node node = decorator.getNode();
Pos pos = decorator.getPos();
Point2D offset = decorator.getPosOffset();
boolean valueInPercentOrPixels = decorator.isValueInPercent();
if (offset == null) {
offset = new Point2D(0, 0);
}
Insets targetPadding = getTargetPadding(targetNode);
double w = node.prefWidth(-1);
double h = node.prefHeight(-1);
double right = 0, left = 0;
double x = offset.getX();
double y = offset.getY();
if (valueInPercentOrPixels) {
x *= w / 100;
y *= h / 100;
}
// TODO: compare the bounds. Will profile it later to see if there is any performance issue.
Bounds boundsInLocal = targetNode.getBoundsInLocal();
Bounds actualBounds = new BoundingBox(boundsInLocal.getMinX() + targetPadding.getLeft(), boundsInLocal.getMinY() + targetPadding.getTop(),
boundsInLocal.getWidth() - targetPadding.getLeft() - targetPadding.getRight(), boundsInLocal.getHeight() - targetPadding.getTop() - targetPadding.getBottom());
Bounds decorationBounds = computeDecorationBounds(targetNode, node, decorator);
boolean inside = actualBounds.intersects(decorationBounds);
if (inside) {
// TODO: need to consider the y as well. If y makes the decoration node completely outside the target node, no need to add left or right padding.
switch (pos) {
case TOP_LEFT:
case CENTER_LEFT:
case BOTTOM_LEFT:
case BASELINE_LEFT:
left = Math.max(0, x + w / 2 - targetPadding.getLeft() + 1); // TODO: 1 is the extra gap, should be calculated automatically
break;
case TOP_RIGHT:
case CENTER_RIGHT:
case BASELINE_RIGHT:
case BOTTOM_RIGHT:
right = Math.max(0, -x + w / 2 - targetPadding.getLeft() + 1); // TODO: 1 is the extra gap, should be calculated automatically
}
}
return new Insets(0, right, 0, left);
}
/**
* Adjust on local bounds, be sure to make this adjustment with padding computation.
*
* @param decorateLocalBounds the local bounds of decorate node.
* @param targetLocalBounds the local bounds of target node.
* @return The new bounds
* @see #adjustPadding
*/
private static Bounds adjustBounds(Bounds decorateLocalBounds, Bounds targetLocalBounds) {
return new BoundingBox(
targetLocalBounds.getMinX() - decorateLocalBounds.getMinX(),
targetLocalBounds.getMinY() - decorateLocalBounds.getMinY(),
targetLocalBounds.getWidth(), targetLocalBounds.getHeight());
}
/**
* Adjust padding on local bounds, be sure to make this adjustment with bounds computation.
*
* @param decorateLocalBounds the local bounds of decorate node.
* @param padding the padding to be adjusted.
* @return The new padding
* @see #adjustBounds
*/
private static Insets adjustPadding(Bounds decorateLocalBounds, Insets padding) {
return new Insets(
padding.getTop() - decorateLocalBounds.getMinY(),
padding.getRight() - decorateLocalBounds.getMinX(),
padding.getBottom() - decorateLocalBounds.getMinY(),
padding.getLeft() - decorateLocalBounds.getMinX());
}
}