/******************************************************************************* * Copyright (c) 2013, 2016 itemis AG and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Alexander Nyßen (itemis AG) - initial API and implementation * Matthias Wienand (itemis AG) - initial API and implementation * *******************************************************************************/ package org.eclipse.gef.fx.nodes; import org.eclipse.gef.fx.utils.Geometry2Shape; import org.eclipse.gef.geometry.planar.AffineTransform; import org.eclipse.gef.geometry.planar.Arc; import org.eclipse.gef.geometry.planar.Ellipse; import org.eclipse.gef.geometry.planar.IGeometry; import org.eclipse.gef.geometry.planar.IScalable; import org.eclipse.gef.geometry.planar.IShape; import org.eclipse.gef.geometry.planar.ITranslatable; import org.eclipse.gef.geometry.planar.Pie; import org.eclipse.gef.geometry.planar.Point; import org.eclipse.gef.geometry.planar.Rectangle; import org.eclipse.gef.geometry.planar.RoundedRectangle; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.Property; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.layout.Region; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.scene.shape.FillRule; import javafx.scene.shape.Path; import javafx.scene.shape.PathElement; import javafx.scene.shape.Shape; import javafx.scene.shape.StrokeLineCap; import javafx.scene.shape.StrokeLineJoin; import javafx.scene.shape.StrokeType; /** * A {@link GeometryNode} is a {@link Node} which can be constructed using an * underlying {@link IGeometry}. It is comparable to a {@link Shape}, while a * {@link GeometryNode} in contrast can be resized. Furthermore, the geometric * bounds of a {@link GeometryNode} can be virtually extended for the purpose of * mouse hit-testing to realize a 'clickable area'. * <p> * Technically, a {@link GeometryNode} is a {@link Region} that internally holds * a {@link Path geometric shape}, which is updated to reflect the given * {@link IGeometry}, and to which all visual properties are delegated. The * 'clickable' area is realized by a transparent, non-mouse transparent overlay * that uses the same {@link IGeometry}, extended by the * {@link #clickableAreaWidthProperty() clickable area width}. * <p> * Please note that because {@link IGeometry} does not support change * notifications itself, changes to the underlying {@link IGeometry} will not be * recognized by the {@link GeometryNode} unless the {@link #geometryProperty() * geometry property} is changed. * * @author anyssen * @author mwienand * * @param <T> * An {@link IGeometry} used to define the geometric shape of this * {@link GeometryNode} */ public class GeometryNode<T extends IGeometry> extends Region { private static final double GEOMETRIC_SHAPE_MIN_WIDTH = 0.01; private static final double GEOMETRIC_SHAPE_MIN_HEIGHT = 0.01; private Path geometricShape = new Path(); private Path clickableAreaShape = null; private DoubleProperty clickableAreaWidth = new SimpleDoubleProperty(); private ObjectProperty<T> geometryProperty = new SimpleObjectProperty<>(); private ChangeListener<T> geometryChangeListener = new ChangeListener<T>() { @Override public void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) { if (newValue != null) { widthProperty().removeListener(widthListener); heightProperty().removeListener(heightListener); layoutXProperty().removeListener(layoutXListener); layoutYProperty().removeListener(layoutYListener); // XXX: We need to clear the size caches; even if we use // computed sizes in the following, if not doing so the // super // call will use stale values. requestLayout(); // update layoutX, layoutY, as well as layout bounds double computedWidth = computePrefWidth(newValue); double computedHeight = computePrefHeight(newValue); if (computedWidth != getWidth() || computedHeight != getHeight()) { GeometryNode.super.resize(computedWidth, computedHeight); } double computedLayoutX = newValue.getBounds().getX() - getStrokeOffset() - getInsets().getLeft(); double computedLayoutY = newValue.getBounds().getY() - getStrokeOffset() - getInsets().getTop(); if (getLayoutX() != computedLayoutX || getLayoutY() != computedLayoutY) { GeometryNode.super.relocate(computedLayoutX, computedLayoutY); } widthProperty().addListener(widthListener); heightProperty().addListener(heightListener); layoutXProperty().addListener(layoutXListener); layoutYProperty().addListener(layoutYListener); // update visuals to reflect changes updateShapes(); } } }; private ChangeListener<Number> widthListener = new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { geometryProperty.removeListener(geometryChangeListener); resizeGeometryToMatchLayoutBoundsSize(newValue.doubleValue(), getHeight()); geometryProperty.addListener(geometryChangeListener); } }; private ChangeListener<Number> heightListener = new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { geometryProperty.removeListener(geometryChangeListener); resizeGeometryToMatchLayoutBoundsSize(getWidth(), newValue.doubleValue()); geometryProperty.addListener(geometryChangeListener); } }; private ChangeListener<Number> layoutXListener = new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { geometryProperty.removeListener(geometryChangeListener); relocateGeometryToMatchLayoutXY(newValue.doubleValue(), getLayoutY()); geometryProperty.addListener(geometryChangeListener); } }; private ChangeListener<Number> layoutYListener = new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { geometryProperty.removeListener(geometryChangeListener); relocateGeometryToMatchLayoutXY(getLayoutX(), newValue.doubleValue()); geometryProperty.addListener(geometryChangeListener); } }; /** * Constructs a new {@link GeometryNode} without an {@link IGeometry}. */ public GeometryNode() { // ensure only our children are mouse-sensitive setPickOnBounds(false); setGeometricShape(geometricShape); // update path elements whenever the geometry property is changed geometryProperty.addListener(geometryChangeListener); // stroke width and type affect the layout bounds, so we have to react // to changes strokeWidthProperty().addListener(new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { T geometry = geometryProperty.get(); if (geometry == null) { return; } resize(prefWidth(-1), prefHeight(-1)); Rectangle geometricBounds = geometry.getBounds(); relocate( geometricBounds.getX() - getStrokeOffset() - getInsets().getLeft(), geometricBounds.getY() - getStrokeOffset() - getInsets().getTop()); } }); strokeTypeProperty().addListener(new ChangeListener<StrokeType>() { @Override public void changed( ObservableValue<? extends StrokeType> observable, StrokeType oldValue, StrokeType newValue) { T geometry = geometryProperty.get(); if (geometry == null) { return; } resize(prefWidth(-1), prefHeight(-1)); Rectangle geometricBounds = geometry.getBounds(); relocate( geometricBounds.getX() - getStrokeOffset() - getInsets().getLeft(), geometricBounds.getY() - getStrokeOffset() - getInsets().getTop()); } }); insetsProperty().addListener(new ChangeListener<Insets>() { @Override public void changed(ObservableValue<? extends Insets> observable, Insets oldValue, Insets newValue) { resize(prefWidth(-1), prefHeight(-1)); } }); // resize geometry in case width and height change widthProperty().addListener(widthListener); heightProperty().addListener(heightListener); // relocate geometry in case layoutX, layoutY change layoutXProperty().addListener(layoutXListener); layoutYProperty().addListener(layoutYListener); } /** * Constructs a new {@link GeometryNode} which displays the given * {@link IGeometry}. * * @param geom * The {@link IGeometry} to display. */ public GeometryNode(T geom) { this(); setGeometry(geom); } /** * Returns a (writable) property that controls the width of the clickable * area. The clickable area is a transparent 'fat' curve overlaying the * actual curve and serving as mouse target. It is only used if the value of * the property is greater than the stroke width of the underlying curve. * * @return A property to control the width of the clickable area of this * connection. */ public DoubleProperty clickableAreaWidthProperty() { return clickableAreaWidth; } private double computeGeometryMinHeight(T geometry) { return geometry instanceof IShape ? GEOMETRIC_SHAPE_MIN_HEIGHT : 0; } private double computeGeometryMinWidth(T geometry) { return geometry instanceof IShape ? GEOMETRIC_SHAPE_MIN_WIDTH : 0; } @Override protected double computeMinHeight(double width) { return computeMinHeight(geometryProperty.get()); } private double computeMinHeight(T geometry) { return computeGeometryMinHeight(geometry) + 2 * getStrokeOffset() + getInsets().getTop() + getInsets().getBottom(); } @Override protected double computeMinWidth(double height) { return computeMinWidth(geometryProperty.get()); } private double computeMinWidth(T geometry) { return computeGeometryMinWidth(geometry) + 2 * getStrokeOffset() + getInsets().getLeft() + getInsets().getRight(); } @Override protected double computePrefHeight(double width) { return computePrefHeight(geometryProperty.get()); } private double computePrefHeight(T geometry) { double geometricPrefHeight = Math.max( geometry != null ? geometry.getBounds().getHeight() : 0, computeGeometryMinHeight(geometry)); return geometricPrefHeight + 2 * getStrokeOffset() + getInsets().getTop() + getInsets().getBottom(); } @Override protected double computePrefWidth(double height) { return computePrefWidth(geometryProperty.get()); } private double computePrefWidth(T geometry) { double geometricPrefWidth = Math.max( geometry != null ? geometry.getBounds().getWidth() : 0, computeGeometryMinWidth(geometry)); return geometricPrefWidth + 2 * getStrokeOffset() + getInsets().getLeft() + getInsets().getRight(); } /** * Provides a {@link Property} holding the fill that is applied to the * {@link Path} internally used by this {@link GeometryNode}. * * @return A (writable) property for the fill of this node. * @see javafx.scene.shape.Shape#fillProperty() */ public final ObjectProperty<Paint> fillProperty() { return geometricShape.fillProperty(); } /** * Provides a {@link Property} holding the fill rule to apply for this * {@link GeometryNode}. * * @return A (writable) property for the fill rule of this node. * @see javafx.scene.shape.Path#fillRuleProperty() */ public final ObjectProperty<FillRule> fillRuleProperty() { return geometricShape.fillRuleProperty(); } /** * Provides a {@link Property} holding the geometry of this * {@link GeometryNode}. * * @return A (writable) property for the geometry of this node. */ public ObjectProperty<T> geometryProperty() { return geometryProperty; } /** * Retrieves the value of the clickable area width property ( * {@link #clickableAreaWidthProperty()}). * * @return The current value of the {@link #clickableAreaWidthProperty()}. */ public double getClickableAreaWidth() { return clickableAreaWidth.get(); } /** * Retrieves the value of the fill property. * * @return The value of the fill property. * * @see javafx.scene.shape.Shape#getFill() */ public final Paint getFill() { return geometricShape.getFill(); } /** * Retrieves the value of the fill rule property. * * @return The value of the fill rule property. * * @see javafx.scene.shape.Path#getFillRule() */ public final FillRule getFillRule() { return geometricShape.getFillRule(); } /** * Returns the {@link Shape} that is used as a delegate to render the * geometry of this {@link GeometryNode}. * * @return The geometric shape used by this {@link GeometryNode}. */ protected Path getGeometricShape() { return geometricShape; } /** * Retrieves the value of the geometry property. * * @return The value of the geometry property. */ public T getGeometry() { return geometryProperty.get(); } /** * Returns the JavaFX {@link Path} that is used to visualize the * {@link IGeometry} of this {@link GeometryNode}. * * @return The JavaFX {@link Path} that is used to visualize the * {@link IGeometry}. */ public Path getPath() { return geometricShape; } private PathElement[] getPathElements() { return Geometry2Shape.toPathElements(geometryProperty.get() .getTransformed(new AffineTransform() .setToTranslation(-getLayoutX(), -getLayoutY())) .toPath()); } /** * Retrieves the value of the stroke property. * * @return The value of the stroke property. * * @see javafx.scene.shape.Shape#getStroke() */ public final Paint getStroke() { return geometricShape.getStroke(); } /** * Retrieves the value of the stroke dash array property. * * @return The value of the stroke dash array property. * * @see javafx.scene.shape.Shape#getStrokeDashArray() */ public final ObservableList<Double> getStrokeDashArray() { return geometricShape.getStrokeDashArray(); } /** * Retrieves the value of the stroke dash offset property. * * @return The value of the stroke dash offset property. * * @see javafx.scene.shape.Shape#getStrokeDashOffset() */ public final double getStrokeDashOffset() { return geometricShape.getStrokeDashOffset(); } /** * Retrieves the value of the stroke line cap property. * * @return The value of the stroke line cap property. * * @see javafx.scene.shape.Shape#getStrokeLineCap() */ public final StrokeLineCap getStrokeLineCap() { return geometricShape.getStrokeLineCap(); } /** * Retrieves the value of the stroke line join property. * * @return The value of the stroke line join property. * * @see javafx.scene.shape.Shape#getStrokeLineJoin() */ public final StrokeLineJoin getStrokeLineJoin() { return geometricShape.getStrokeLineJoin(); } /** * Retrieves the value of the stroke miter limit property. * * @return The value of the stroke miter limit property. * * @see javafx.scene.shape.Shape#getStrokeMiterLimit() */ public final double getStrokeMiterLimit() { return geometricShape.getStrokeMiterLimit(); } private double getStrokeOffset() { double offset = 0; if (geometricShape.getStroke() != null && geometricShape.getStrokeType() != StrokeType.INSIDE) { offset = (geometricShape.getStrokeType() == StrokeType.CENTERED ? 0.5 : 1) * geometricShape.getStrokeWidth(); } return offset; } /** * Retrieves the value of the stroke type property. * * @return The value of the stroke type property. * * @see javafx.scene.shape.Shape#getStrokeType() */ public final StrokeType getStrokeType() { return geometricShape.getStrokeType(); } /** * Retrieves the value of the stroke width property. * * @return The value of the stroke width property. * * @see javafx.scene.shape.Shape#getStrokeWidth() */ public final double getStrokeWidth() { return geometricShape.getStrokeWidth(); } @Override public boolean isResizable() { return true; } /** * Retrieves the value of the smooth property. * * @return The value of the smooth property. * @see javafx.scene.shape.Shape#isSmooth() */ public final boolean isSmooth() { return geometricShape.isSmooth(); } @Override public void relocate(double x, double y) { // prevent unnecessary updates layoutXProperty().removeListener(layoutXListener); layoutYProperty().removeListener(layoutYListener); super.relocate(x, y); layoutXProperty().addListener(layoutXListener); layoutYProperty().addListener(layoutYListener); relocateGeometryToMatchLayoutXY(x, y); } /** * Relocates the {@link #geometryProperty() geometry}. * * @param x * The new x coordinate * @param y * The new y coordinate */ @SuppressWarnings("unchecked") public void relocateGeometry(double x, double y) { T geometry = geometryProperty.getValue(); Rectangle geometryBounds = geometry.getBounds(); if (geometry instanceof ITranslatable) { geometryProperty.set(((ITranslatable<T>) geometry).getTranslated( x - geometryBounds.getX(), y - geometryBounds.getY())); } else { geometryProperty.set((T) geometry.getTransformed( new AffineTransform().translate((x - geometryBounds.getX()), (y - geometryBounds.getY())))); } } private void relocateGeometryToMatchLayoutXY(double layoutX, double layoutY) { // guard against null geometry T geometry = geometryProperty.get(); if (geometry == null) { return; } // geometry has to reflect final position relative to layout bounds, // which are based on (0, 0) geometryProperty.removeListener(geometryChangeListener); relocateGeometry(layoutX + getStrokeOffset() + getInsets().getLeft(), layoutY + getStrokeOffset() + getInsets().getTop()); geometryProperty.addListener(geometryChangeListener); updateShapes(); } @Override public void resize(double width, double height) { if (width < minWidth(-1)) { throw new IllegalArgumentException( "Cannot resize below mininmal width " + minWidth(-1) + ", so " + width + " is no valid width"); } if (height < minHeight(-1)) { throw new IllegalArgumentException( "Cannot resize below mininmal height " + minHeight(-1) + ", so " + height + " is no valid height"); } // prevent unnecessary updates widthProperty().removeListener(widthListener); heightProperty().removeListener(heightListener); super.resize(width, height); widthProperty().addListener(widthListener); heightProperty().addListener(heightListener); resizeGeometryToMatchLayoutBoundsSize(width, height); } /** * Resizes the {@link #geometryProperty()} to the given width and height. * * @param width * The new width. * @param height * The new height. */ @SuppressWarnings("unchecked") public void resizeGeometry(double width, double height) { T geometry = geometryProperty.getValue(); double geometryMinWidth = computeGeometryMinWidth(geometry); if (width < geometryMinWidth) { throw new IllegalArgumentException( "Cannot resize geometry below " + geometryMinWidth + ", so " + width + " is no valid width."); } double geometryMinHeight = computeGeometryMinHeight(geometry); if (height < geometryMinHeight) { throw new IllegalArgumentException( "Cannot resize geometry below " + geometryMinHeight + ", so " + height + " is no valid height."); } if (geometry instanceof Rectangle) { geometryProperty.set((T) ((Rectangle) geometry).getCopy() .setSize(width, height)); } else if (geometry instanceof RoundedRectangle) { geometryProperty.set((T) ((RoundedRectangle) geometry).getCopy() .setSize(width, height)); } else if (geometry instanceof Ellipse) { geometryProperty.set( (T) ((Ellipse) geometry).getCopy().setSize(width, height)); } else if (geometry instanceof Pie) { geometryProperty .set((T) ((Pie) geometry).getCopy().setSize(width, height)); } else if (geometry instanceof Arc) { geometryProperty .set((T) ((Arc) geometry).getCopy().setSize(width, height)); } else { Rectangle geometricBounds = geometry.getBounds(); double sx = geometricBounds.getWidth() == 0 ? 1 : width / geometricBounds.getWidth(); double sy = geometricBounds.getHeight() == 0 ? 1 : height / geometricBounds.getHeight(); if (geometry instanceof IScalable) { // Line, Polyline, PolyBezier, BezierCurve, CubicCurve, // QuadraticCurve, Polygon, CurvedPolygon, Region, and Ring are // not directly resizable but scalable geometryProperty.set(((IScalable<T>) geometry).getScaled(sx, sy, geometricBounds.getX(), geometricBounds.getY())); } else { // apply transform to path Point boundsOrigin = new Point(geometricBounds.getX(), geometricBounds.getY()); geometryProperty.setValue((T) geometry .getTransformed(new AffineTransform(1, 0, 0, 1, -boundsOrigin.x, -boundsOrigin.y)) .getTransformed(new AffineTransform(sx, 0, 0, sy, 0, 0)) .getTransformed(new AffineTransform(1, 0, 0, 1, boundsOrigin.x, boundsOrigin.y))); } } } private void resizeGeometryToMatchLayoutBoundsSize(double layoutBoundsWidth, double layoutBoundsHeight) { // guard against null geometry T geometry = geometryProperty.get(); if (geometry == null) { return; } // Disable listening to geometry changes while determine new geometry // size (to match given visual bounds size) geometryProperty.removeListener(geometryChangeListener); // System.out.println("Resizing to " + width + ", " + height); // the target width/height for the layout bounds (of the geometric // shape) is without the insets and stroke double strokeOffset = getStrokeOffset(); double geometryWidth = layoutBoundsWidth - getInsets().getLeft() - getInsets().getRight() - 2 * strokeOffset; double geometryMinWidth = computeGeometryMinWidth(geometry); if (geometryWidth < geometryMinWidth) { geometryWidth = geometryMinWidth; } double geometryHeight = layoutBoundsHeight - getInsets().getTop() - getInsets().getBottom() - 2 * strokeOffset; double geometryMinHeight = computeGeometryMinHeight(geometry); if (geometryHeight < geometryMinHeight) { geometryHeight = geometryMinHeight; } // System.out.println( // "Resize Geometry to " + geometryWidth + ", " + geometryHeight); resizeGeometry(geometryWidth, geometryHeight); // System.out.println("... " + geometryProperty.get().getBounds()); // update geometry of underlying path (which should invalidate the // layout bounds) geometryProperty.addListener(geometryChangeListener); updateShapes(); } /** * Sets the value of the property {@link #clickableAreaWidthProperty() * clickable area width} property. * * @param clickableAreaWidth * The new value of the {@link #clickableAreaWidthProperty() * clickable area width} property. */ public void setClickableAreaWidth(double clickableAreaWidth) { this.clickableAreaWidth.set(clickableAreaWidth); } /** * Sets the value of the fill property. * * @param value * The new value of the fill property. * * @see javafx.scene.shape.Shape#setFill(javafx.scene.paint.Paint) */ public final void setFill(Paint value) { geometricShape.setFill(value); } /** * Sets the value of the fill rule property. * * @param value * The new value of the fill rule property. * * @see javafx.scene.shape.Path#setFillRule(javafx.scene.shape.FillRule) */ public final void setFillRule(FillRule value) { geometricShape.setFillRule(value); } /** * Sets the geometric shape used by this {@link GeometryNode}. * * @param geometricShape * The geometric shape. */ protected void setGeometricShape(final Path geometricShape) { // add geometric shape getChildren().add(geometricShape); // Unfortunately those methods in Node that are responsible for handling // CSS style (getStyleClass(), getStyle()) are final, thus cannot be // delegated to the geometric shape. As Parent does not support CSS // styling itself, we can at least 'forward' them. getStyleClass().addListener(new ListChangeListener<String>() { @Override public void onChanged( javafx.collections.ListChangeListener.Change<? extends String> c) { // delegate style classes to geometric shape while (c.next()) { if (c.wasPermutated() || c.wasUpdated()) { geometricShape.getStyleClass().clear(); geometricShape.getStyleClass().addAll(getStyleClass()); } else { geometricShape.getStyleClass() .removeAll(c.getRemoved()); geometricShape.getStyleClass() .addAll(c.getAddedSubList()); } } } }); styleProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) { geometricShape.setStyle(newValue); } }); // ensure clickable area is added/removed as needed clickableAreaWidth.addListener(new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { if (newValue != null && newValue.doubleValue() > geometricShape .getStrokeWidth() && clickableAreaShape == null && geometryProperty.getValue() != null) { // create and configure clickable area shape clickableAreaShape = new Path(getPathElements()); clickableAreaShape .setId("clickable area of GeometryNode " + this); clickableAreaShape.setStroke(Color.TRANSPARENT); clickableAreaShape.setMouseTransparent(false); clickableAreaShape.strokeWidthProperty() .bind(clickableAreaWidthProperty()); // add clickable area and binding only if its really used getChildren().add(clickableAreaShape); } else if ((newValue == null || newValue .doubleValue() <= geometricShape.getStrokeWidth()) && clickableAreaShape != null) { getChildren().remove(clickableAreaShape); clickableAreaShape.strokeWidthProperty().unbind(); clickableAreaShape = null; } } }); } /** * Sets the {@link IGeometry} of this {@link GeometryNode} to the given * value. * * @param geometry * The new {@link IGeometry} for this {@link GeometryNode}. */ public void setGeometry(T geometry) { this.geometryProperty.setValue(geometry); } /** * Sets the value of the smooth property. * * @param value * The new value of the smooth property. * * @see javafx.scene.shape.Shape#setSmooth(boolean) */ public final void setSmooth(boolean value) { geometricShape.setSmooth(value); } /** * * Sets the value of the stroke property. * * @param value * The new value of the stroke property. * * @see javafx.scene.shape.Shape#setStroke(javafx.scene.paint.Paint) */ public final void setStroke(Paint value) { geometricShape.setStroke(value); } /** * Sets the value of the stroke dash offset property. * * @param value * The new value of the stroke dash offset property. * * @see javafx.scene.shape.Shape#setStrokeDashOffset(double) */ public final void setStrokeDashOffset(double value) { geometricShape.setStrokeDashOffset(value); } /** * Sets the value of the stroke line cap property. * * @param value * The new value of the stroke line cap property. * * @see javafx.scene.shape.Shape#setStrokeLineCap(javafx.scene.shape.StrokeLineCap) */ public final void setStrokeLineCap(StrokeLineCap value) { geometricShape.setStrokeLineCap(value); } /** * Sets the value of the stroke line join property. * * @param value * The new value of the stroke line join property. * * @see javafx.scene.shape.Shape#setStrokeLineJoin(javafx.scene.shape.StrokeLineJoin) */ public final void setStrokeLineJoin(StrokeLineJoin value) { geometricShape.setStrokeLineJoin(value); } /** * Sets the value of the stroke miter limit property. * * @param value * The new value of the stroke miter limit property. * * @see javafx.scene.shape.Shape#setStrokeMiterLimit(double) */ public final void setStrokeMiterLimit(double value) { geometricShape.setStrokeMiterLimit(value); } /** * Sets the value of the stroke type property. * * @param value * The new value of the stroke type property. * * @see javafx.scene.shape.Shape#setStrokeType(javafx.scene.shape.StrokeType) */ public final void setStrokeType(StrokeType value) { geometricShape.setStrokeType(value); } /** * Sets the value of the stroke width property. * * @param value * The new value of the stroke width property. * * @see javafx.scene.shape.Shape#setStrokeWidth(double) */ public final void setStrokeWidth(double value) { geometricShape.setStrokeWidth(value); } /** * Provides a {@link Property} holding the smooth value to apply for this * {@link GeometryNode}. * * @return A (writable) property for the smooth value of this node. * * @see javafx.scene.shape.Shape#smoothProperty() */ public final BooleanProperty smoothProperty() { return geometricShape.smoothProperty(); } /** * Provides a {@link Property} holding the stroke dash offset to apply for * this {@link GeometryNode}. * * @return A (writable) property for the stroke dash offset of this node. * * @see javafx.scene.shape.Shape#strokeDashOffsetProperty() */ public final DoubleProperty strokeDashOffsetProperty() { return geometricShape.strokeDashOffsetProperty(); } /** * Provides a {@link Property} holding the stroke line cap to apply for this * {@link GeometryNode}. * * @return A (writable) property for the stroke line cap of this node. * * @see javafx.scene.shape.Shape#strokeLineCapProperty() */ public final ObjectProperty<StrokeLineCap> strokeLineCapProperty() { return geometricShape.strokeLineCapProperty(); } /** * Provides a {@link Property} holding the stroke line join to apply for * this {@link GeometryNode}. * * @return A (writable) property for the stroke line join of this node. * * @see javafx.scene.shape.Shape#strokeLineJoinProperty() */ public final ObjectProperty<StrokeLineJoin> strokeLineJoinProperty() { return geometricShape.strokeLineJoinProperty(); } /** * Provides a {@link Property} holding the stroke miter limit to apply for * this {@link GeometryNode}. * * @return A (writable) property for the stroke miter limit of this node. * * @see javafx.scene.shape.Shape#strokeMiterLimitProperty() */ public final DoubleProperty strokeMiterLimitProperty() { return geometricShape.strokeMiterLimitProperty(); } /** * Defines parameters of a stroke that is drawn around the outline of a * Shape using the settings of the specified Paint. The default value is * Color.BLACK. * * @return A writable {@link Property} to control the stroke of this * {@link GeometryNode}. * * @see javafx.scene.shape.Shape#strokeProperty() */ public final ObjectProperty<Paint> strokeProperty() { return geometricShape.strokeProperty(); } /** * Provides a {@link Property} holding the stroke type to apply for this * {@link GeometryNode}. * * @return A (writable) property for the stroke type of this node. * * @see javafx.scene.shape.Shape#strokeTypeProperty() */ public final ObjectProperty<StrokeType> strokeTypeProperty() { return geometricShape.strokeTypeProperty(); } /** * Provides a {@link Property} holding the stroke width to apply for this * {@link GeometryNode}. * * @return A (writable) property for the stroke width of this node. * * @see javafx.scene.shape.Shape#strokeWidthProperty() */ public final DoubleProperty strokeWidthProperty() { return geometricShape.strokeWidthProperty(); } /** * Updates the visual representation (Path) of this GeometryNode. This is * done automatically when setting the geometry. But in case you change * properties of a geometry, you have to call this method in order to update * its visual counter part. */ private void updateShapes() { if (clickableAreaShape != null) { updateShapes(geometricShape, clickableAreaShape); } else { updateShapes(geometricShape); } } private void updateShapes(Path... paths) { PathElement[] pathElements = getPathElements(); for (Path p : paths) { p.getElements().setAll(pathElements); } } }