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

org.controlsfx.control.SnapshotView 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) 2014, 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.control;

import static javafx.beans.binding.Bindings.and;
import static javafx.beans.binding.Bindings.isNotNull;
import static javafx.beans.binding.Bindings.notEqual;
import impl.org.controlsfx.skin.SnapshotViewSkin;
import impl.org.controlsfx.tools.rectangle.Rectangles2D;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableMap;
import javafx.css.CssMetaData;
import javafx.css.StyleConverter;
import javafx.css.Styleable;
import javafx.css.StyleableDoubleProperty;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;

/**
 * A {@code SnapshotView} is a control which allows the user to select an area of a node in the typical manner used by
 * picture editors and crate snapshots of the selection.
 * 

* While holding the left mouse key down, a rectangular selection can be drawn. This selection can be moved, resized in * eight cardinal directions and removed. Additionally, the selection's ratio can be fixed in which case the user's * resizing will be limited such that the ratio is always upheld. *

* The area where the selection is possible is either this entire control or limited to the displayed node. * *

Screenshots

*
Screenshot of SnapshotView
* *

Code Samples

* The following snippet creates a new instance with the ControlsFX logo loaded from the web, sets a selected area and * fixes its ratio: * *
 * ImageView controlsFxView = new ImageView(
 *         "http://cache.fxexperience.com/wp-content/uploads/2013/05/ControlsFX.png");
 * SnapshotView snapshotView = new SnapshotView(controlsFxView);
 * snapshotView.setSelection(33, 50, 100, 100);
 * snapshotView.setFixedSelectionRatio(1); // (this is actually the default value)
 * snapshotView.setSelectionRatioFixed(true);
 * 
* *

Functionality Overview

* * This is just a vague overview. The linked properties provide a more detailed explanation. * *

Node

* * The node which this control displays is held by the {@link #nodeProperty() node} property. * *

Selection

* * There are several properties which interact to manage and indicate the selection. * *
State
*
    *
  • the selection is held by the {@link #selectionProperty() selection} property *
  • the {@link #hasSelectionProperty() hasSelection} property indicates whether a selection exists *
  • the {@link #selectionActiveProperty() selectionActive} property indicates whether the current selection is active * (it is only displayed if it is); by default this property is updated by this control which is determined by the * {@link #selectionActivityManagedProperty() selectionActivityManaged} property *
* *
Interaction
*
    *
  • if the selection is changing due to the user interacting with the control, this is indicated by the * {@link #selectionChangingProperty() selectionChanging} property *
  • whether the user can select any area of the control or only one above the node is determined by the * {@link #selectionAreaBoundaryProperty() selectionAreaBoundary} property *
  • with the {@link #selectionMouseTransparentProperty() selectionMouseTransparent} property the control can be made * mouse transparent so the user can interact with the displayed node *
  • the selection's ratio of width to height can be fixed with the {@link #selectionRatioFixedProperty() * selectionRatioFixed} and the {@link #fixedSelectionRatioProperty() fixedSelectionRatio} properties *
* *
Visualization
*
    *
  • {@link #selectionAreaFillProperty() selectionAreaFill} property for the selected area's paint *
  • {@link #selectionBorderPaintProperty() selectionBorderPaint} property for the selection border's paint *
  • {@link #selectionBorderWidthProperty() selectionBorderWidth} property for the selection border's width *
  • {@link #unselectedAreaFillProperty() unselectedAreaFill} property for the area outside of the selection *
  • {@link #unselectedAreaBoundaryProperty() unselectedAreaBoundary} property which defined what the unselected area * covers *
*/ public class SnapshotView extends ControlsFXControl { /** * The maximal divergence between a selection's ratio and the {@link #fixedSelectionRatioProperty() * fixedselectionRatio} for the selection to still have the correct ratio (see {@link #hasCorrectRatio(Rectangle2D) * hasCorrectRatio}). *

* The divergence is expressed relative to the {@code fixedselectionRatio}. */ public static final double MAX_SELECTION_RATIO_DIVERGENCE = 1e-6; /** * The key of the {@link #getProperties() property} which is used to update {@link #selectionChangingProperty() * selectionChanging}. */ public static final String SELECTION_CHANGING_PROPERTY_KEY = SnapshotView.class.getCanonicalName() + ".selection_changing"; //$NON-NLS-1$ /* ************************************************************************ * * * Attributes & Properties * * * **************************************************************************/ // NODE /** * @see #nodeProperty() */ private final ObjectProperty node; // SELECTION /** * @see #selectionProperty() */ private final ObjectProperty selection; /** * @see #hasSelectionProperty() */ private final BooleanProperty hasSelection; /** * @see #selectionActiveProperty() */ private final BooleanProperty selectionActive; /** * @see #selectionChangingProperty() */ private final BooleanProperty selectionChanging; /** * @see #selectionRatioFixedProperty() */ private final BooleanProperty selectionRatioFixed; /** * @see #fixedSelectionRatioProperty() */ private final DoubleProperty fixedSelectionRatio; // META /** * @see #selectionAreaBoundaryProperty() */ private final ObjectProperty selectionAreaBoundary; /** * @see #selectionActivityManagedProperty() */ private final BooleanProperty selectionActivityManaged; /** * @see #selectionMouseTransparentProperty() */ private final BooleanProperty selectionMouseTransparent; // VISUALIZATION /** * @see #unselectedAreaBoundaryProperty() */ private final ObjectProperty unselectedAreaBoundary; /** * @see #selectionBorderPaintProperty() */ private final ObjectProperty selectionBorderPaint; /** * @see #selectionBorderWidthProperty() */ private final DoubleProperty selectionBorderWidth; /** * @see #selectionAreaFillProperty() */ private final ObjectProperty selectionAreaFill; /** * @see #unselectedAreaFillProperty() */ private final ObjectProperty unselectedAreaFill; /* ************************************************************************ * * * Construction * * * **************************************************************************/ /** * Creates a new SnapshotView. */ public SnapshotView() { getStyleClass().setAll(DEFAULT_STYLE_CLASS); // NODE node = new SimpleObjectProperty<>(this, "node"); //$NON-NLS-1$ // SELECTION selection = new SimpleObjectProperty(this, "selection") { //$NON-NLS-1$ @Override public void set(Rectangle2D selection) { if (!isSelectionValid(selection)) { throw new IllegalArgumentException("The selection \"" + selection + "\" is invalid. " + //$NON-NLS-1$ //$NON-NLS-2$ "Check the comment on 'SnapshotView.selectionProperty()' " + //$NON-NLS-1$ "for all criteria a selection must fulfill."); //$NON-NLS-1$ } super.set(selection); } }; hasSelection = new SimpleBooleanProperty(this, "hasSelection", false); //$NON-NLS-1$ hasSelection.bind(and(isNotNull(selection), notEqual(Rectangle2D.EMPTY, selection))); selectionActive = new SimpleBooleanProperty(this, "selectionActive", false); //$NON-NLS-1$ selectionChanging = new SimpleBooleanProperty(this, "selectionChanging", false); //$NON-NLS-1$ selectionRatioFixed = new SimpleBooleanProperty(this, "selectionRatioFixed", false); //$NON-NLS-1$ fixedSelectionRatio = new SimpleDoubleProperty(this, "fixedSelectionRatio", 1) { //$NON-NLS-1$ @Override public void set(double newValue) { if (newValue <= 0) { throw new IllegalArgumentException("The fixed selection ratio must be positive."); //$NON-NLS-1$ } super.set(newValue); } }; // META selectionAreaBoundary = createStylableObjectProperty( this, "selectionAreaBoundary", Boundary.CONTROL, Css.SELECTION_AREA_BOUNDARY); //$NON-NLS-1$ selectionActivityManaged = new SimpleBooleanProperty(this, "selectionActivityManaged", true); //$NON-NLS-1$ selectionMouseTransparent = new SimpleBooleanProperty(this, "selectionMouseTransparent", false); //$NON-NLS-1$ // VISUALIZATION unselectedAreaBoundary = createStylableObjectProperty( this, "unselectedAreaBoundary", Boundary.CONTROL, Css.UNSELECTED_AREA_BOUNDARY); //$NON-NLS-1$ selectionBorderPaint = createStylableObjectProperty( this, "selectionBorderPaint", Color.WHITESMOKE, Css.SELECTION_BORDER_PAINT); //$NON-NLS-1$ selectionBorderWidth = createStylableDoubleProperty( this, "selectionBorderWidth", 2.5, Css.SELECTION_BORDER_WIDTH); //$NON-NLS-1$ selectionAreaFill = createStylableObjectProperty( this, "selectionAreaFill", Color.TRANSPARENT, Css.SELECTION_AREA_FILL); //$NON-NLS-1$ unselectedAreaFill = createStylableObjectProperty( this, "unselectedAreaFill", new Color(0, 0, 0, 0.5), Css.UNSELECTED_AREA_FILL); //$NON-NLS-1$ addStateUpdatingListeners(); // update selection when resizing new SelectionSizeUpdater().enableResizing(); } /** * Adds listeners to the properties which update the control's state. */ private void addStateUpdatingListeners() { // update the selection activity state when the selection is set selection.addListener((o, oldValue, newValue) -> updateSelectionActivityState()); // ratio selectionRatioFixed.addListener((o, oldValue, newValue) -> { boolean valueChangedToTrue = !oldValue && newValue; if (valueChangedToTrue) { fixSelectionRatio(); } }); fixedSelectionRatio.addListener((o, oldValue, newValue) -> { if (isSelectionRatioFixed()) { fixSelectionRatio(); } }); // set selection changing according to the values set in the property map listenToProperty( getProperties(), SELECTION_CHANGING_PROPERTY_KEY, (Boolean value) -> selectionChanging.set(value)); } /** * Listens to the specified properties. When a pair with the specified key is added, it is processed. If the value * has the correct type, it is given to the specified consumer. Even if the type does not match, it is removed from * the map. * * @param properties * the {@link ObservableMap} which contains the properties; typically {@link Control#getProperties()} * @param key * the key for whose value is listened * @param processValue * the {@link Consumer} for the new value */ private static void listenToProperty( ObservableMap properties, Object key, Consumer processValue) { Objects.requireNonNull(properties, "The argument 'properties' must not be null."); //$NON-NLS-1$ Objects.requireNonNull(key, "The argument 'key' must not be null."); //$NON-NLS-1$ Objects.requireNonNull(processValue, "The argument 'processValue' must not be null."); //$NON-NLS-1$ @SuppressWarnings("unchecked") MapChangeListener listener = change -> { boolean addedForKey = change.wasAdded() && Objects.equals(key, change.getKey()); if (addedForKey) { // give the value to the consumer if it has the correct type try { // note that this cast does nothing except to calm the compiler // (hence the warning which had to be suppressed) T newValue = (T) change.getValueAdded(); // this is where the actual exception is created processValue.accept(newValue); } catch (ClassCastException e) { // the value was of the wrong type so it can't be processed by the consumer // -> do nothing } // remove the value from the properties map properties.remove(key); } }; properties.addListener(listener); } /** * Creates a new SnapshotView using the specified node. * * @param node * the node to show after construction */ public SnapshotView(Node node) { this(); setNode(node); } /* ************************************************************************ * * * Public Methods * * * **************************************************************************/ /** * Transforms the {@link #selectionProperty() selection} to node coordinates by calling * {@link #transformToNodeCoordinates(Rectangle2D) transformToNodeCoordinates}. * * @return a {@link Rectangle2D} which expresses the selection in the node's coordinates * @throws IllegalStateException * if {@link #nodeProperty() node} is {@code null} or {@link #hasSelection() hasSelection} is * {@code false} * @see #transformToNodeCoordinates(Rectangle2D) */ public Rectangle2D transformSelectionToNodeCoordinates() { if (!hasSelection()) { throw new IllegalStateException( "The selection can not be transformed if it does not exist (check 'hasSelection()')."); //$NON-NLS-1$ } return transformToNodeCoordinates(getSelection()); } /** * Transforms the specified area's coordinates to coordinates relative to the node. (The node's coordinate system * has its origin in the upper left corner of the node.) * * @param area * the {@link Rectangle2D} which will be transformed (must not be {@code null}); its coordinates will be * interpreted relative to the control (like the {@link #selectionProperty() selection}) * @return a {@link Rectangle2D} with the same width and height as the specified {@code area} but with coordinates * which are relative to the current {@link #nodeProperty() node} * @throws IllegalStateException * if {@link #nodeProperty() node} is {@code null} */ public Rectangle2D transformToNodeCoordinates(Rectangle2D area) throws IllegalStateException { Objects.requireNonNull(area, "The argument 'area' must not be null."); //$NON-NLS-1$ if (getNode() == null) { throw new IllegalStateException( "The selection can not be transformed if the node is null (check 'getNode()')."); //$NON-NLS-1$ } // get the offset from the node's bounds Bounds nodeBounds = getNode().getBoundsInParent(); double xOffset = nodeBounds.getMinX(); double yOffset = nodeBounds.getMinY(); // the coordinates of the transformed selection double minX = area.getMinX() - xOffset; double minY = area.getMinY() - yOffset; return new Rectangle2D(minX, minY, area.getWidth(), area.getHeight()); } /** * Creates a snapshot of the selected area of the node. * * @return the {@link WritableImage} that holds the rendered selection * @throws IllegalStateException * if {@link #nodeProperty() node} is {@code null} or {@link #hasSelection() hasSelection} is * {@code false} * @see Node#snapshot */ public WritableImage createSnapshot() throws IllegalStateException { // make sure the node and the selection exist if (getNode() == null) { throw new IllegalStateException("No snapshot can be created if the node is null (check 'getNode()')."); //$NON-NLS-1$ } if (!hasSelection()) { throw new IllegalStateException( "No snapshot can be created if there is no selection (check 'hasSelection()')."); //$NON-NLS-1$ } SnapshotParameters parameters = new SnapshotParameters(); parameters.setViewport(getSelection()); return createSnapshot(parameters); } /** * Creates a snapshot of the node with the specified parameters. * * @param parameters * the {@link SnapshotParameters} used for the snapshot (must not be {@code null}); the viewport will be * interpreted relative to this control (like the {@link #selectionProperty() selection}) * @return the {@link WritableImage} that holds the rendered viewport * @throws IllegalStateException * if {@link #nodeProperty() node} is {@code null} * @see Node#snapshot */ public WritableImage createSnapshot(SnapshotParameters parameters) throws IllegalStateException { // make sure the node and the snapshot parameters exist Objects.requireNonNull(parameters, "The argument 'parameters' must not be null."); //$NON-NLS-1$ if (getNode() == null) { throw new IllegalStateException("No snapshot can be created if the node is null (check 'getNode()')."); //$NON-NLS-1$ } // take the snapshot return getNode().snapshot(parameters, null); } /* ************************************************************************ * * * Model State * * * **************************************************************************/ /** * Updates the {@link #selectionActiveProperty() selectionActive} property if the * {@link #selectionActivityManagedProperty() selectionActivityManaged} property indicates that it is managed by * this control. */ private void updateSelectionActivityState() { boolean userManaged = !isSelectionActivityManaged(); if (userManaged) { return; } boolean selectionActive = getSelection() != null && getSelection() != Rectangle2D.EMPTY; setSelectionActive(selectionActive); } /** * Resizes the current selection (if it exists) to the {@link #fixedSelectionRatioProperty() fixedSelectionRatio}. */ private void fixSelectionRatio() { boolean noSelectionToFix = getNode() == null || !hasSelection(); if (noSelectionToFix) { return; } Rectangle2D selectionBounds = getSelectionBounds(); Rectangle2D resizedSelection = Rectangles2D.fixRatioWithinBounds( getSelection(), getFixedSelectionRatio(), selectionBounds); selection.set(resizedSelection); } /** * * @return the bounds of the current selection according to the {@link #selectionAreaBoundaryProperty() * selectionAreaBoundary}. */ private Rectangle2D getSelectionBounds() { Boundary boundary = getSelectionAreaBoundary(); switch (boundary) { case CONTROL: return new Rectangle2D(0, 0, getWidth(), getHeight()); case NODE: return Rectangles2D.fromBounds(getNode().getBoundsInParent()); default: throw new IllegalArgumentException("The boundary '" + boundary + "' is not fully implemented yet."); //$NON-NLS-1$ //$NON-NLS-2$ } } /** * Checks whether the specified selection is valid. This includes checking whether the selection is in bounds and * has the correct ratio (if the ratio is fixed). * * @param selection * the selection to check as a {@link Rectangle2D} * @return {@code true} if the selection is valid; {@code false} otherwise */ private boolean isSelectionValid(Rectangle2D selection) { // empty selections are valid boolean emptySelection = selection == null || selection == Rectangle2D.EMPTY; if (emptySelection) { return true; } // check values if (!valuesFinite(selection)) { return false; } // check bounds if (!inBounds(selection)) { return false; } // check ratio if (!hasCorrectRatio(selection)) { return false; } return true; } /** * Indicates whether the specified selection has only finite values (e.g. width and height). * * @param selection * the selection as a {@link Rectangle2D} * @return {@code true} if the selection has only finite values. */ private static boolean valuesFinite(Rectangle2D selection) { return Double.isFinite(selection.getMinX()) && Double.isFinite(selection.getMinY()) && Double.isFinite(selection.getWidth()) && Double.isFinite(selection.getHeight()); } /** * Indicates whether the specified selection is inside the bounds determined by the * {@link #selectionAreaBoundaryProperty() selectionAreaBoundary} property. * * @param selection * the non-null and non-empty selection as a {@link Rectangle2D} * @return {@code true} if the selection is fully contained in the bounds; otherwise {@code false} */ private boolean inBounds(Rectangle2D selection) { Boundary boundary = getSelectionAreaBoundary(); switch (boundary) { case CONTROL: return inBounds(selection, getBoundsInLocal()); case NODE: if (getNode() == null) { return false; } else { return inBounds(selection, getNode().getBoundsInParent()); } default: throw new IllegalArgumentException("The boundary '" + boundary + "' is not fully implemented yet."); //$NON-NLS-1$ //$NON-NLS-2$ } } /** * Indicates whether the specified selection is inside the specified bounds. * * @param selection * the selection as a {@link Rectangle2D} * @param bounds * the {@link Bounds} to check the selection against * @return {@code true} if the selection is fully contained in the bounds; otherwise {@code false} */ private static boolean inBounds(Rectangle2D selection, Bounds bounds) { return bounds.getMinX() <= selection.getMinX() && bounds.getMinY() <= selection.getMinY() && selection.getMaxX() <= bounds.getMaxX() && selection.getMaxY() <= bounds.getMaxY(); } /** * Indicates whether the specified selection has the correct ratio (which depends on whether the ratio is even * {@link #selectionRatioFixedProperty() fixed}). * * @param selection * the selection to check as a {@link Rectangle2D} * @return {@code true} if the selection has the correct ratio. */ private boolean hasCorrectRatio(Rectangle2D selection) { if (!isSelectionRatioFixed()) { return true; } double ratio = selection.getWidth() / selection.getHeight(); // compute the divergence relative to the fixed selection ratio double ratioDivergence = Math.abs(1 - ratio / getFixedSelectionRatio()); return ratioDivergence <= MAX_SELECTION_RATIO_DIVERGENCE; } /* ************************************************************************ * * * Style Sheet & Skin Handling * * * **************************************************************************/ /** * The name of the style class used in CSS for instances of this class. */ private static final String DEFAULT_STYLE_CLASS = "snapshot-view"; //$NON-NLS-1$ /** {@inheritDoc} */ @Override public String getUserAgentStylesheet() { return getUserAgentStylesheet(SnapshotView.class, "snapshot-view.css"); //$NON-NLS-1$ } /** * Creates a {@link StyleableDoubleProperty} with the specified arguments. * * @param bean * the {@link Property#getBean() bean} the created property belongs to * @param name * the property's {@link Property#getName() name} * @param initialValue * the property's initial value * @param cssMetaData * the {@link CssMetaData} for the created property * @return a {@link StyleableDoubleProperty} */ private static StyleableDoubleProperty createStylableDoubleProperty( Object bean, String name, double initialValue, CssMetaData cssMetaData) { return new StyleableDoubleProperty(initialValue) { @Override public Object getBean() { return bean; } @Override public String getName() { return name; } @Override public CssMetaData getCssMetaData() { return cssMetaData; } }; } /** * Creates a {@link StyleableObjectProperty} with the specified arguments. * * @param bean * the {@link Property#getBean() bean} the created property belongs to * @param name * the property's {@link Property#getName() name} * @param initialValue * the property's initial value * @param cssMetaData * the {@link CssMetaData} for the created property * @return a {@link StyleableObjectProperty} */ private static StyleableObjectProperty createStylableObjectProperty( Object bean, String name, T initialValue, CssMetaData cssMetaData) { return new StyleableObjectProperty(initialValue) { @Override public Object getBean() { return bean; } @Override public String getName() { return name; } @Override public CssMetaData getCssMetaData() { return cssMetaData; } }; } /** * Creates an instance of {@link CssMetaData} with the specified arguments. * * @param getProperty * a function from the {@link Styleable} which owns the styled property to the property styled by the * returned {@code CssMetaData} * @param cssPropertyName * the name by which the styled property is referenced in CSS files * @param styleConverter * the {@link StyleConverter} used to convert the CSS parsed value to a Java object * @return an instance of {@link CssMetaData} */ private static CssMetaData createCssMetaData( Function> getProperty, String cssPropertyName, StyleConverter styleConverter) { return new CssMetaData(cssPropertyName, styleConverter) { @Override public boolean isSettable(S styleable) { final Property property = getProperty.apply(styleable); return property != null && !property.isBound(); } @Override @SuppressWarnings("unchecked") public StyleableProperty getStyleableProperty(S styleable) { return (StyleableProperty) getProperty.apply(styleable); } }; } /** * The class which holds this control's {@link CssMetaData} for the different {@link StyleableProperty * StyleableProperties}. */ @SuppressWarnings({ "javadoc", "unchecked" }) private static class Css { public static final CssMetaData SELECTION_AREA_BOUNDARY = createCssMetaData( snapshotView -> snapshotView.selectionAreaBoundary, "-fx-selection-area-boundary", //$NON-NLS-1$ (StyleConverter) StyleConverter.getEnumConverter(Boundary.class)); public static final CssMetaData UNSELECTED_AREA_BOUNDARY = createCssMetaData( snapshotView -> snapshotView.unselectedAreaBoundary, "-fx-unselected-area-boundary", //$NON-NLS-1$ (StyleConverter) StyleConverter.getEnumConverter(Boundary.class)); public static final CssMetaData SELECTION_BORDER_PAINT = createCssMetaData( snapshotView -> snapshotView.selectionBorderPaint, "-fx-selection-border-paint", //$NON-NLS-1$ StyleConverter.getPaintConverter()); public static final CssMetaData SELECTION_BORDER_WIDTH = createCssMetaData( snapshotView -> snapshotView.selectionBorderWidth, "-fx-selection-border-width", //$NON-NLS-1$ StyleConverter.getSizeConverter()); public static final CssMetaData SELECTION_AREA_FILL = createCssMetaData( snapshotView -> snapshotView.selectionAreaFill, "-fx-selection-area-fill", //$NON-NLS-1$ StyleConverter.getPaintConverter()); public static final CssMetaData UNSELECTED_AREA_FILL = createCssMetaData( snapshotView -> snapshotView.unselectedAreaFill, "-fx-unselected-area-fill", //$NON-NLS-1$ StyleConverter.getPaintConverter()); /** * The {@link CssMetaData} associated with this class, which includes the {@code CssMetaData} of its super * classes. */ public static final List> CSS_META_DATA; static { final List> styleables = new ArrayList<>(Control.getClassCssMetaData()); styleables.add(SELECTION_AREA_BOUNDARY); styleables.add(UNSELECTED_AREA_BOUNDARY); styleables.add(SELECTION_BORDER_PAINT); styleables.add(SELECTION_BORDER_WIDTH); styleables.add(SELECTION_AREA_FILL); styleables.add(UNSELECTED_AREA_FILL); CSS_META_DATA = Collections.unmodifiableList(styleables); } } /** * @return the {@link CssMetaData} associated with this class, which includes the {@code CssMetaData} of its super * classes */ public static List> getClassCssMetaData() { return Css.CSS_META_DATA; } @Override public List> getControlCssMetaData() { return getClassCssMetaData(); } @Override protected Skin createDefaultSkin() { return new SnapshotViewSkin(this); } /* ************************************************************************ * * * Property Access * * * **************************************************************************/ // NODE /** * The {@link Node} which will be displayed in the center of this control. *

* The node's {@link Node#boundsInParentProperty() boundsInParent} show its relative position inside this control. * Since the {@link #selectionProperty() selection} property also uses this control as its reference coordinate * system, the bounds can be used to compute which area of the node is selected. *

* If this control or the node behaves strangely when resized, try embedding the original node in a {@link Pane} and * setting the pane here. * * @return the property holding the displayed node */ public final ObjectProperty nodeProperty() { return node; } /** * @return the displayed node * @see #nodeProperty() */ public final Node getNode() { return nodeProperty().get(); } /** * @param node * the node to display * @see #nodeProperty() */ public final void setNode(Node node) { nodeProperty().set(node); } // SELECTION /** * The current selection as a {@link Rectangle2D}. As such an instance is immutable a new one must be set to chane * the selection. *

* The rectangle's coordinates are interpreted relative to this control. The top left corner is the origin (0, 0) * and the lower right corner is ({@link #widthProperty() width}, {@link #heightProperty() height}). It is * guaranteed that the selection always lies within these bounds. If the control is resized, so is the selection. If * a selection which violates these bounds is set, an {@link IllegalArgumentException} is thrown. *

* The same is true if the {@link #selectionAreaBoundaryProperty() selectionAreaBoundary} is set to {@code NODE} but * with the stricter condition that the selection must lie within the {@link #nodeProperty() node}'s * {@link Node#boundsInParentProperty() boundsInParent}. *

* If the selection ratio is {@link #selectionRatioFixedProperty() fixed}, any new selection must have the * {@link #fixedSelectionRatioProperty() fixedSelectionRatio}. Otherwise, an {@code IllegalArgumentException} is * thrown. *

* An {@code IllegalArgumentException} is also thrown if not all of the selection's values (e.g. width and height) * are finite. *

* The selection might be {@code null} or {@link Rectangle2D#EMPTY} in which case no selection is displayed and * {@link #hasSelectionProperty() hasSelection} is {@code false}. * * @return the property holding the current selection * @see #hasSelectionProperty() */ public final ObjectProperty selectionProperty() { return selection; } /** * @return the current selection * @see #selectionProperty() */ public final Rectangle2D getSelection() { return selectionProperty().get(); } /** * @param selection * the new selection * @throws IllegalArgumentException * if the selection is out of the bounds defined by the {@link #selectionAreaBoundaryProperty() * selectionAreaBoundary} or the selection ratio is {@link #selectionRatioFixedProperty() fixed} and the * new selection does not have the {@link #fixedSelectionRatioProperty() fixedSelectionRatio}. * @see #selectionProperty() */ public final void setSelection(Rectangle2D selection) { selectionProperty().set(selection); } /** * Creates a new {@link Rectangle2D} from the specified arguments and sets it as the new * {@link #selectionProperty() selection}. It will have ({@code upperLeftX}, {@code upperLeftY}) as its upper left * point and span {@code width} to the right and {@code height} down. * * @param upperLeftX * the x coordinate of the selection's upper left point * @param upperLeftY * the y coordinate of the selection's upper left point * @param width * the selection's width * @param height * the selection's height * @throws IllegalArgumentException * if the selection is out of the bounds defined by the {@link #selectionAreaBoundaryProperty() * selectionAreaBoundary} or the selection ratio is {@link #selectionRatioFixedProperty() fixed} and the * new selection does not have the {@link #fixedSelectionRatioProperty() fixedSelectionRatio}. * @see #selectionProperty() * */ public final void setSelection(double upperLeftX, double upperLeftY, double width, double height) { selectionProperty().set(new Rectangle2D(upperLeftX, upperLeftY, width, height)); } /** * Indicates whether there currently is a selection. This will be {@code false} if the {@link #selectionProperty() * selection} property holds {@code null} or {@link Rectangle2D#EMPTY} . * * @return a property indicating whether there currently is a selection */ public final ReadOnlyBooleanProperty hasSelectionProperty() { return hasSelection; } /** * @return whether there currently is a selection * @see #hasSelectionProperty() */ public final boolean hasSelection() { return hasSelectionProperty().get(); } /** * Indicates whether the selection is currently active. Only an active selection will be displayed by the control. *

* See {@link #selectionActivityManagedProperty() selectionActivityManaged} for documentation on how this property * might be changed by this control. * * @return the property indicating whether the selection is active */ public final BooleanProperty selectionActiveProperty() { return selectionActive; } /** * @return whether the selection is active * @see #selectionActiveProperty() */ public final boolean isSelectionActive() { return selectionActiveProperty().get(); } /** * @param selectionActive * the new selection active status * @see #selectionActiveProperty() */ public final void setSelectionActive(boolean selectionActive) { selectionActiveProperty().set(selectionActive); } /** * Indicates whether the {@link #selectionProperty() selection} is currently changing due to user interaction with * the control. It will be set to {@code true} when changing the selection begins and set to {@code false} when it * ends. *

* If a selection is set by the code using this control (e.g. by calling {@link #setSelection(Rectangle2D) * setSelection}) this property does not change its value. * * @return a property indicating whether the selection is changing by user interaction */ public final ReadOnlyBooleanProperty selectionChangingProperty() { return selectionChanging; } /** * @return whether the selection is changing by user interaction * @see #selectionChangingProperty() */ public final boolean isSelectionChanging() { return selectionChangingProperty().get(); } /** * Indicates whether the ratio of the {@link #selectionProperty() selection} is fixed. *

* By default this property is {@code false} and the user interacting with this control can make arbitrary * selections with any ratio of width to height. If it is {@code true}, the user is limited to making selections * with the ratio defined by the {@link #fixedSelectionRatioProperty() fixedSelectionRatio} property. If the ratio * is fixed and a selection with a different ratio is set, an {@link IllegalArgumentException} is thrown. *

* If a selection exists and this property is set to {@code true}, the selection is immediately resized to the * currently set ratio. * * @defaultValue {@code false} * @return the property indicating whether the selection ratio is fixed */ public final BooleanProperty selectionRatioFixedProperty() { return selectionRatioFixed; } /** * @return whether the selection ratio is fixed * @see #selectionRatioFixedProperty() */ public final boolean isSelectionRatioFixed() { return selectionRatioFixedProperty().get(); } /** * @param selectionRatioFixed * whether the selection ratio will be fixed * @see #selectionRatioFixedProperty() */ public final void setSelectionRatioFixed(boolean selectionRatioFixed) { selectionRatioFixedProperty().set(selectionRatioFixed); } /** * The value to which the selection ratio is fixed. The ratio is defined as {@code width / height} and its value * must be strictly positive. *

* If {@link #selectionRatioFixedProperty() selectionRatioFixed} is {@code true}, this ratio will be upheld by all * changes made by user interaction with this control. If the ratio is fixed and a selection is set by code (e.g. by * calling {@link #setSelection(Rectangle2D) setSelection}), this ratio is checked and if violated an * {@link IllegalArgumentException} is thrown. *

* If a selection exists and {@code selectionRatioFixed} is set to {@code true}, the selection is immediately * resized to this ratio. Similarly, if a selection exists and its ratio is fixed, setting a new value resizes the * selection to the new ratio. * * @defaultValue 1.0 * @return a property containing the fixed selection ratio */ public final DoubleProperty fixedSelectionRatioProperty() { return fixedSelectionRatio; } /** * @return the fixedSelectionRatio, which will always be a strictly positive value * @see #fixedSelectionRatioProperty() */ public final double getFixedSelectionRatio() { return fixedSelectionRatioProperty().get(); } /** * @param fixedSelectionRatio * the fixed selection ratio to set * @throws IllegalArgumentException * if {@code fixedSelectionRatio} is not strictly positive * @see #fixedSelectionRatioProperty() */ public final void setFixedSelectionRatio(double fixedSelectionRatio) { fixedSelectionRatioProperty().set(fixedSelectionRatio); } // META /** * Indicates which {@link Boundary} is set for the area the user can select. *

* By default the user can select any area of the control. If this should be limited to the area over the displayed * node instead, this property can be set to {@link Boundary#NODE NODE}. If the value is changed from * {@code CONTROL} to {@code NODE} a possibly existing selection is resized accordingly. *

* If the boundary is set to {@code NODE}, this is also respected when a new {@link #selectionProperty() selection} * is set. This means the condition for the new selection's coordinates is made stricter and setting a selection out * of the node's bounds (instead of only out of the control's bounds) throws an {@link IllegalArgumentException}. *

* Note that this does not change the reference coordinate system! The selection's coordinates are still * interpreted relative to the {@link #nodeProperty() node}'s {@link Node#boundsInParentProperty() boundsInParent}. * * @defaultValue {@link Boundary#CONTROL CONTROL} * @return the property indicating the {@link Boundary} for the area the user can select */ public final ObjectProperty selectionAreaBoundaryProperty() { return selectionAreaBoundary; } /** * @return the {@link Boundary} for the area the user can select */ public final Boundary getSelectionAreaBoundary() { return selectionAreaBoundaryProperty().get(); } /** * @param selectionAreaBoundary * the new {@link Boundary} for the area the user can select */ public final void setSelectionAreaBoundary(Boundary selectionAreaBoundary) { selectionAreaBoundaryProperty().set(selectionAreaBoundary); } /** * Indicates whether the value of the {@link #selectionActiveProperty() selectionActive} property is managed by this * control. *

* If this property is set to {@code true} (which is the default) this control will update the * {@code selectionActive} property immediately after a new selection is set: if the new selection is {@code null} * or {@link Rectangle2D#EMPTY}, it will be set to {@code false}; otherwise to {@code true}. *

* If this property is {@code false} this control will never change {@code selectionActive}'s value. In this case it * must be managed by the using code but it is possible to unidirectionally bind it to another property without this * control interfering. * * @defaultValue {@code true} * @return the property indicating whether the value of the {@link #selectionActiveProperty() selectionActive} * property is managed by this control */ public final BooleanProperty selectionActivityManagedProperty() { return selectionActivityManaged; } /** * @return whether the selection activity is managed by this control * @see #selectionActivityManagedProperty() */ public final boolean isSelectionActivityManaged() { return selectionActivityManagedProperty().get(); } /** * @param selectionActivityManaged * whether the selection activity will be managed by this control * @see #selectionActivityManagedProperty() */ public final void setSelectionActivityManaged(boolean selectionActivityManaged) { selectionActivityManagedProperty().set(selectionActivityManaged); } /** * Indicates whether the overlay which displays the selection is mouse transparent. *

* By default all mouse events are captured by this control and used to interact with the selection. If this * property is set to {@code true}, this behavior changes and the user is able to interact with the displayed * {@link #nodeProperty() node}. * * @defaultValue {@code false} * @return the property indicating whether the selection is mouse transparent */ public final BooleanProperty selectionMouseTransparentProperty() { return selectionMouseTransparent; } /** * @return whether the selection is mouse transparent * @see #selectionMouseTransparentProperty() */ public final boolean isSelectionMouseTransparent() { return selectionMouseTransparentProperty().get(); } /** * @param selectionMouseTransparent * whether the selection will be mouse transparent * @see #selectionMouseTransparentProperty() */ public final void setSelectionMouseTransparent(boolean selectionMouseTransparent) { selectionMouseTransparentProperty().set(selectionMouseTransparent); } // VISUALIZATION /** * Indicates which {@link Boundary} is set for the visualization of the unselected area (i.e. the area outside of * the selection rectangle). *

* If it is set to {@link Boundary#CONTROL CONTROL} (which is the default), the unselected area covers the whole * control. *

* If it is set to {@link Boundary#NODE NODE}, the area only covers the displayed {@link #nodeProperty() node}. In * most cases this only makes sense if the {@link #selectionAreaBoundaryProperty() selectionAreaBoundary} is also * set to {@code NODE}. * * @defaultValue {@link Boundary#CONTROL} * @return the property defining the {@link Boundary} of the unselected area */ public final ObjectProperty unselectedAreaBoundaryProperty() { return unselectedAreaBoundary; } /** * @return the {@link Boundary} for the unselected area * @see #unselectedAreaBoundaryProperty() */ public final Boundary getUnselectedAreaBoundary() { return unselectedAreaBoundaryProperty().get(); } /** * @param unselectedAreaBoundary * the new {@link Boundary} for the unselected area * @see #unselectedAreaBoundaryProperty() */ public final void setUnselectedAreaBoundary(Boundary unselectedAreaBoundary) { unselectedAreaBoundaryProperty().set(unselectedAreaBoundary); } /** * Determines the visualization of the selection's border. * * @defaultValue {@link Color#WHITESMOKE} * @return the property holding the {@link Paint} of the selection border * @see #selectionBorderWidthProperty() */ public final ObjectProperty selectionBorderPaintProperty() { return selectionBorderPaint; } /** * @return the {@link Paint} of the selection border * @see #selectionBorderPaintProperty() */ public final Paint getSelectionBorderPaint() { return selectionBorderPaintProperty().get(); } /** * @param selectionBorderPaint * the new {@link Paint} of the selection border * @see #selectionBorderPaintProperty() */ public final void setSelectionBorderPaint(Paint selectionBorderPaint) { selectionBorderPaintProperty().set(selectionBorderPaint); } /** * Determines the width of the selection's border. The border is always painted to the outside of the selected area, * i.e. the selected area is never covered by the border. * * @defaultValue 2.5 * @return the property defining the selection border's width * @see #selectionBorderPaintProperty() * @see javafx.scene.shape.Shape#strokeWidthProperty() Shape.strokeWidthProperty() */ public final DoubleProperty selectionBorderWidthProperty() { return selectionBorderWidth; } /** * @return the selection border width * @see #selectionBorderWidthProperty() */ public final double getSelectionBorderWidth() { return selectionBorderWidthProperty().get(); } /** * @param selectionBorderWidth * the selection border width to set * @see #selectionBorderWidthProperty() */ public final void setSelectionBorderWidth(double selectionBorderWidth) { selectionBorderWidthProperty().set(selectionBorderWidth); } /** * Determines the visualization of the selected area. * * @defaultValue {@link Color#TRANSPARENT} * @return the property holding the {@link Paint} of the selected area */ public final ObjectProperty selectionAreaFillProperty() { return selectionAreaFill; } /** * @return the {@link Paint} of the selected area * @see #selectionAreaFillProperty() */ public final Paint getSelectionAreaFill() { return selectionAreaFillProperty().get(); } /** * @param selectionAreaFill * the new {@link Paint} of the selected area * @see #selectionAreaFillProperty() */ public final void setSelectionAreaFill(Paint selectionAreaFill) { selectionAreaFillProperty().set(selectionAreaFill); } /** * Determines the visualization of the area outside of the selection. * * @defaultValue {@link Color#BLACK black} with {@link Color#getOpacity() opacity} 0.5 * @return the property holding the {@link Paint} of the area outside of the selection */ public final ObjectProperty unselectedAreaFillProperty() { return unselectedAreaFill; } /** * @return the {@link Paint} of the area outside of the selection * @see #unselectedAreaFillProperty() */ public final Paint getUnselectedAreaFill() { return unselectedAreaFillProperty().get(); } /** * @param unselectedAreaFill * the new {@link Paint} of the area outside of the selection * @see #unselectedAreaFillProperty() */ public final void setUnselectedAreaFill(Paint unselectedAreaFill) { unselectedAreaFillProperty().set(unselectedAreaFill); } /* ************************************************************************ * * * Inner Classes * * * **************************************************************************/ /** * The {@link SnapshotView#selectionAreaBoundaryProperty() selectionArea}, in which the user can create a selection, * and the {@link SnapshotView#unselectedAreaBoundaryProperty() unselectedArea}, in which the unselected area is * visualized, are limited to a certain area of the control. This area's boundary is represented by this enum. * */ public static enum Boundary { /** * The boundary is this control's bound. */ CONTROL, /** * The boundary is the displayed node's bound. */ NODE, } /** * Updates the size of the {@link SnapshotView#selectionProperty() selection} whenever necessary. This is the case * if the {@link SnapshotView#selectionAreaBoundaryProperty() selectionAreaBoundary} is set to * {@link Boundary#CONTROL CONTROL} and the control is resized or when it is set to {@link Boundary#NODE NODE} and * the node is changed or resized. * */ private class SelectionSizeUpdater { /* * If the 'selectionAreaBoundary' is set to 'CONTROL', the selection is only updated when the control changes * its width or height. If it is set to 'NODE', the selection is resized whenever the node or its * 'boundsInParent' change. * For both cases methods exist which resize the selection. The listeners which call those methods are only * added to the corresponding properties when the matching boundary is selected. */ // CONTROL /** * Calls {@link #resizeSelectionToNewControlWidth(ObservableValue, Number, Number) * updateSelectionToNewControlWidth} whenever the control's width changes. */ private final ChangeListener resizeSelectionToNewControlWidthListener; /** * Calls {@link #resizeSelectionToNewControlHeight(ObservableValue, Number, Number) * updateSelectionToNewControlWidth} whenever the control's height changes. */ private final ChangeListener resizeSelectionToNewControlHeightListener; // NODE /** * Calls {@link #updateSelectionToNewNode(ObservableValue, Node, Node) updateSelectionToNewNode} whenever a new * {@link SnapshotView#nodeProperty() node} is set. */ private final ChangeListener updateSelectionToNodeListener; /** * Calls {@link #resizeSelectionToNewNodeBounds(ObservableValue, Bounds, Bounds) updateSelectionToNewNodeBounds} * whenever the node's {@link Node#boundsInParentProperty() boundsInParent} change. */ private final ChangeListener resizeSelectionToNewNodeBoundsListener; // CONSTRUCTION /** * Creates a new selection size updater. */ public SelectionSizeUpdater() { // create listeners which point to methods resizeSelectionToNewControlWidthListener = this::resizeSelectionToNewControlWidth; resizeSelectionToNewControlHeightListener = this::resizeSelectionToNewControlHeight; updateSelectionToNodeListener = this::updateSelectionToNewNode; resizeSelectionToNewNodeBoundsListener = this::resizeSelectionToNewNodeBounds; } // ENABLE RESIZING /** * Enables resizing of the control. */ public void enableResizing() { // only resize if the selection is not null enableResizingForBoundary(getSelectionAreaBoundary()); selectionAreaBoundary.addListener((o, oldBoundary, newBoundary) -> enableResizingForBoundary(newBoundary)); } /** * Enables resizing for the specified boundary. * * @param boundary * the {@link Boundary} for which the control will be resized. */ private void enableResizingForBoundary(Boundary boundary) { switch (boundary) { case CONTROL: enableResizingForControl(); break; case NODE: enableResizingForNode(); break; default: throw new IllegalArgumentException("The boundary '" + boundary + "' is not fully implemented yet."); //$NON-NLS-1$ //$NON-NLS-2$ } } /** * Enables resizing if the {@link SnapshotView#selectionAreaBoundary selectionAreaBoundary} is * {@link Boundary#CONTROL CONTROL}. */ private void enableResizingForControl() { // remove listeners for node and its bounds node.removeListener(updateSelectionToNodeListener); if (getNode() != null) { getNode().boundsInParentProperty().removeListener(resizeSelectionToNewNodeBoundsListener); } // add listener for the control's size widthProperty().addListener(resizeSelectionToNewControlWidthListener); heightProperty().addListener(resizeSelectionToNewControlHeightListener); resizeSelectionFromNodeToControl(); } /** * Enables resizing if the {@link SnapshotView#selectionAreaBoundary selectionAreaBoundary} is * {@link Boundary#NODE NODE}. */ private void enableResizingForNode() { // remove listeners for the control's size widthProperty().removeListener(resizeSelectionToNewControlWidthListener); heightProperty().removeListener(resizeSelectionToNewControlHeightListener); // add listener for the node's bounds and for new nodes if (getNode() != null) { getNode().boundsInParentProperty().addListener(resizeSelectionToNewNodeBoundsListener); } node.addListener(updateSelectionToNodeListener); resizeSelectionFromControlToNode(); } // RESIZE TO CONTROL /** * Resizes the current {@link SnapshotView#selectionProperty() selection} from the node's to the control's * bounds. */ private void resizeSelectionFromNodeToControl() { if (getNode() == null) { setSelection(null); } else { // transform the selection from the control's to the node's bounds Rectangle2D controlBounds = new Rectangle2D(0, 0, getWidth(), getHeight()); Rectangle2D nodeBounds = Rectangles2D.fromBounds(getNode().getBoundsInParent()); resizeSelectionToNewBounds(nodeBounds, controlBounds); } } /** * Resizes the current {@link SnapshotView#selectionProperty() selection} from the control's specified old width * to its specified new width. *

* Designed to be used as a lambda method reference. * * @param o * the {@link ObservableValue} which changed its value * @param oldWidth * the control's old width * @param newWidth * the control's new width */ private void resizeSelectionToNewControlWidth( @SuppressWarnings("unused") ObservableValue o, Number oldWidth, Number newWidth) { Rectangle2D oldBounds = new Rectangle2D(0, 0, oldWidth.doubleValue(), getHeight()); Rectangle2D newBounds = new Rectangle2D(0, 0, newWidth.doubleValue(), getHeight()); resizeSelectionToNewBounds(oldBounds, newBounds); } /** * Resizes the current {@link SnapshotView#selectionProperty() selection} from the control's specified old * height to its specified new height. *

* Designed to be used as a lambda method reference. * * @param o * the {@link ObservableValue} which changed its value * @param oldHeight * the control's old height * @param newHeight * the control's new height */ private void resizeSelectionToNewControlHeight( @SuppressWarnings("unused") ObservableValue o, Number oldHeight, Number newHeight) { Rectangle2D oldBounds = new Rectangle2D(0, 0, getWidth(), oldHeight.doubleValue()); Rectangle2D newBounds = new Rectangle2D(0, 0, getWidth(), newHeight.doubleValue()); resizeSelectionToNewBounds(oldBounds, newBounds); } // RESIZE TO NODE /** * Resizes the current {@link SnapshotView#selectionProperty() selection} from the control's to the node's * bounds */ private void resizeSelectionFromControlToNode() { if (getNode() == null) { setSelection(null); } else { // transform the selection from the control's to the node's bounds Rectangle2D controlBounds = new Rectangle2D(0, 0, getWidth(), getHeight()); Rectangle2D nodeBounds = Rectangles2D.fromBounds(getNode().getBoundsInParent()); resizeSelectionToNewBounds(controlBounds, nodeBounds); } } /** * Moves the {@link #resizeSelectionToNewNodeBoundsListener} from the specified old to the specified new node's * {@link Node#boundsInParentProperty() boundsInParent} property and resizes the current * {@link SnapshotView#selectionProperty() selection} from the old to the new node's bounds. *

* Designed to be used as a lambda method reference. * * @param o * the {@link ObservableValue} which changed its value * @param oldNode * the old node * @param newNode * the new node */ private void updateSelectionToNewNode( @SuppressWarnings("unused") ObservableValue o, Node oldNode, Node newNode) { // move the bounds listener from the old to the new node if (oldNode != null) { oldNode.boundsInParentProperty().removeListener(resizeSelectionToNewNodeBoundsListener); } if (newNode != null) { newNode.boundsInParentProperty().addListener(resizeSelectionToNewNodeBoundsListener); } // update selection if (oldNode == null || newNode == null) { // if one of the nodes is null, set no selection setSelection(null); } else { // transform the current selection resizeSelectionToNewNodeBounds(null, oldNode.getBoundsInParent(), newNode.getBoundsInParent()); } } /** * Resizes the current {@link SnapshotView#selectionProperty() selection} from the specified old to the * specified new bounds of the {@link SnapshotView#nodeProperty() node}. * * @param o * the {@link ObservableValue} which changed its value * @param oldBounds * the node's old bounds * @param newBounds * the node's new bounds */ private void resizeSelectionToNewNodeBounds( @SuppressWarnings("unused") ObservableValue o, Bounds oldBounds, Bounds newBounds) { resizeSelectionToNewBounds(Rectangles2D.fromBounds(oldBounds), Rectangles2D.fromBounds(newBounds)); } // GENERAL RESIZING /** * If this control {@link SnapshotView#hasSelection() has a selection} it is resized from the specified old to * the specified new bounds. * * @param oldBounds * the {@link SnapshotView#selectionProperty() selection}'s old bounds as a {@link Rectangle2D} * @param newBounds * the {@link SnapshotView#selectionProperty() selection}'s new bounds as a {@link Rectangle2D} */ private void resizeSelectionToNewBounds(Rectangle2D oldBounds, Rectangle2D newBounds) { if (!hasSelection()) { return; } Rectangle2D newSelection = transformSelectionToNewBounds(getSelection(), oldBounds, newBounds); if (isSelectionValid(newSelection)) { setSelection(newSelection); } else { setSelection(null); } } /** * Returns a new selection which is a transformation of the specified old selection. The transformation is such * that the new selection's "relative position" in the specified new bounds is the same as the old selection's * relative position in the specified old bounds. *

* Here, "relative position" is a representation of the selection where the coordinates of its upper left point * and its width and height are expressed in a percentage of its bounds. Those percentages are the same for * "old selection in old bounds" and "returned selection in new bounds" * * @param oldSelection * the selection to be transformed as a {@link Rectangle2D} * @param oldBounds * the {@code oldSelection}'s old bounds as a {@link Rectangle2D} * @param newBounds * the {@code oldSelection}'s new bounds as a {@link Rectangle2D} * @return s {@link Rectangle2D} which is the transformation of the old selection to the new bounds */ private Rectangle2D transformSelectionToNewBounds( Rectangle2D oldSelection, Rectangle2D oldBounds, Rectangle2D newBounds) { Point2D newSelectionCenter = computeNewSelectionCenter(oldSelection, oldBounds, newBounds); double widthRatio = newBounds.getWidth() / oldBounds.getWidth(); double heightRatio = newBounds.getHeight() / oldBounds.getHeight(); if (isSelectionRatioFixed()) { double newArea = (oldSelection.getWidth() * widthRatio) * (oldSelection.getHeight() * heightRatio); double ratio = getFixedSelectionRatio(); return Rectangles2D.forCenterAndAreaAndRatioWithinBounds(newSelectionCenter, newArea, ratio, newBounds); } else { double newWidth = oldSelection.getWidth() * widthRatio; double newHeight = oldSelection.getHeight() * heightRatio; return Rectangles2D.forCenterAndSize(newSelectionCenter, newWidth, newHeight); } } /** * Computes a point with the same relative position in the specified new bounds as the specified old selection's * center point in the specified old bounds. (See * {@link #transformSelectionToNewBounds(Rectangle2D, Rectangle2D, Rectangle2D) transformSelectionToNewBounds} * for a definition of "relative position"). * * @param oldSelection * the selection whose center point is the base for the returned center point as a * {@link Rectangle2D} * @param oldBounds * the bounds of the old selection as a {@link Rectangle2D} * @param newBounds * the bounds for the new selection as a {@link Rectangle2D} * @return a {@link Point2D} with the same relative position in the new bounds as the old selection's center * point in the old bounds */ private Point2D computeNewSelectionCenter(Rectangle2D oldSelection, Rectangle2D oldBounds, Rectangle2D newBounds) { Point2D oldSelectionCenter = Rectangles2D.getCenterPoint(oldSelection); Point2D oldBoundsCenter = Rectangles2D.getCenterPoint(oldBounds); Point2D oldSelectionCenterOffset = oldSelectionCenter.subtract(oldBoundsCenter); double widthRatio = newBounds.getWidth() / oldBounds.getWidth(); double heightRatio = newBounds.getHeight() / oldBounds.getHeight(); Point2D newSelectionCenterOffset = new Point2D( oldSelectionCenterOffset.getX() * widthRatio, oldSelectionCenterOffset.getY() * heightRatio); Point2D newBoundsCenter = Rectangles2D.getCenterPoint(newBounds); Point2D newSelectionCenter = newBoundsCenter.add(newSelectionCenterOffset); return newSelectionCenter; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy