/*******************************************************************************
* Copyright (c) 2014, 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 java.util.Arrays;
import java.util.List;
import org.eclipse.gef.geometry.convert.fx.FX2Geometry;
import org.eclipse.gef.geometry.convert.fx.Geometry2FX;
import org.eclipse.gef.geometry.planar.AffineTransform;
import javafx.animation.FadeTransition;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Orientation;
import javafx.geometry.Point2D;
import javafx.geometry.Side;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundImage;
import javafx.scene.layout.BackgroundPosition;
import javafx.scene.layout.BackgroundRepeat;
import javafx.scene.layout.BackgroundSize;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Affine;
import javafx.util.Duration;
/**
* An {@link InfiniteCanvas} provides a means to render a portion of a
* hypothetically infinite canvas, on which arbitrary contents can be placed.
*
* <pre>
* +----------------+
* |content area |
* | |
* | +----------------+
* | |visible area |
* | | |
* | +----------------+
* | |
* +----------------+
* </pre>
* <p>
* The size of the {@link InfiniteCanvas} itself determines the visible area,
* i.e. it is reflected in its {@link #layoutBoundsProperty()}. The content area
* is determined by the (visible) bounds of the {@link #getContentGroup()} that
* contains the content elements. These bounds can be accessed via the
* {@link #contentBoundsProperty()}.
* <p>
* By default, scrollbars are shown when the content area exceeds the visible
* area. They allow to navigate the {@link #scrollableBoundsProperty()}, which
* resembles the union of the content area and the visible area. The horizontal
* and vertical scroll offsets are controlled by the
* {@link #horizontalScrollOffsetProperty()} and
* {@link #verticalScrollOffsetProperty()}. The appearance of scrollbars can be
* controlled with the following properties:
* <ul>
* <li>The {@link #horizontalScrollBarPolicyProperty()} determines the
* horizontal {@link ScrollBarPolicy}.
* <li>The {@link #verticalScrollBarPolicyProperty()} determines the vertical
* {@link ScrollBarPolicy}.
* </ul>
* <p>
* An arbitrary transformation can be applied to the contents that is controlled
* by the {@link #contentTransformProperty()}. It is unrelated to scrolling,
* i.e. translating the content does not change the scroll offset.
* <p>
* A background grid is rendered behind the contents per default. It always
* covers the complete visible area and can be enabled/disabled and customized
* via a set of properties:
* <ul>
* <li>The {@link #showGridProperty()} determines whether or not to show the
* background grid
* <li>The {@link #zoomGridProperty()} determines whether or not to zoom the
* background grid with the contents.
* <li>The {@link #gridCellWidthProperty()} determines the grid cell width.
* <li>The {@link #gridCellHeightProperty()} determines the grid cell height.
* </ul>
* <p>
* Internally, an {@link InfiniteCanvas} consists of four layers:
*
* <pre>
* +--------------------------------+
* |scrollbar group |
* +--------------------------------+
* |overlay group |
* +--------------------------------+
* |scrolled pane (with sub-layers) |
* +--------------------------------+
* |underlay group |
* +--------------------------------+
* </pre>
* <ul>
* <li>The {@link #getUnderlayGroup()} is rendered at the bottom, it is neither
* affected by the {@link #horizontalScrollOffsetProperty()} and
* {@link #verticalScrollOffsetProperty()} nor by the
* {@link #contentTransformProperty()}.
* <li>The {@link #getScrolledPane()} is rendered above the
* {@link #getUnderlayGroup()} and contains sub-layers. The
* {@link #getScrolledPane()} and its sub-layers are affected by the
* {@link #horizontalScrollOffsetProperty()} and
* {@link #verticalScrollOffsetProperty()}.
* <li>The {@link #getOverlayGroup()} is rendered above the
* {@link #getScrolledPane()}. It is neither affected by the
* {@link #horizontalScrollOffsetProperty()} and
* {@link #verticalScrollOffsetProperty()} nor by the
* {@link #contentTransformProperty()}.
* <li>The {@link #getScrollBarGroup()} is rendered above the
* {@link #getOverlayGroup()}. It contains the scrollbars.
* </ul>
* The {@link #getScrolledPane()} internally consists of the following four
* sub-layers:
*
* <pre>
* +--------------------------------+
* |scrolled overlay group |
* +--------------------------------+
* |content group |
* +--------------------------------+
* |scrolled underlay group |
* +--------------------------------+
* |grid canvas |
* +--------------------------------+
* </pre>
* <ul>
* <li>The {@link #getGridCanvas()} is rendered at the bottom of the
* {@link #getScrolledPane()}.
* <li>The {@link #getScrolledUnderlayGroup()} is rendered above the
* {@link #getGridCanvas()}.
* <li>The {@link #getContentGroup()} is rendered above the
* {@link #getScrolledUnderlayGroup()}. It is affected by the
* {@link #contentTransformProperty()}.
* <li>The {@link #getScrolledOverlayGroup()} is rendered above the
* {@link #getContentGroup()}.
* </ul>
*
* @author anyssen
* @author mwienand
*/
public class InfiniteCanvas extends Region {
/**
* The default {@link Color} that is used to draw grid points.
*/
public static final Color DEFAULT_GRID_POINT_COLOR = Color.DARKGREY;
/**
* The default grid cell width.
*/
public static final int DEFAULT_GRID_CELL_WIDTH = 10;
/**
* The default grid cell height.
*/
public static final int DEFAULT_GRID_CELL_HEIGHT = 10;
// background grid
private Region grid;
private Affine gridTransform = new Affine();
private final IntegerProperty gridCellHeightProperty = new SimpleIntegerProperty(
DEFAULT_GRID_CELL_WIDTH);
private final IntegerProperty gridCellWidthProperty = new SimpleIntegerProperty(
DEFAULT_GRID_CELL_HEIGHT);
private final ReadOnlyObjectWrapper<Affine> gridTransformProperty = new ReadOnlyObjectWrapper<>(
new Affine());
private final BooleanProperty showGridProperty = new SimpleBooleanProperty(
true);
private final BooleanProperty zoomGridProperty = new SimpleBooleanProperty(
true);
private final ChangeListener<Number> repaintGridTileListener = new ChangeListener<Number>() {
@Override
public void changed(final ObservableValue<? extends Number> observable,
final Number oldValue, final Number newValue) {
repaintGrid();
}
};
private ChangeListener<Affine> updateGridTransformListener = new ChangeListener<Affine>() {
@Override
public void changed(ObservableValue<? extends Affine> observable,
Affine oldValue, Affine newValue) {
updateGridTransform(newValue);
}
};
// clipping
private Rectangle clippingRectangle = new Rectangle();
private final BooleanProperty clipContentProperty = new SimpleBooleanProperty(
true);
// scrollbars
private Group scrollBarGroup;
private ScrollBar horizontalScrollBar;
private ScrollBar verticalScrollBar;
private final ObjectProperty<ScrollBarPolicy> horizontalScrollBarPolicyProperty = new SimpleObjectProperty<>(
ScrollBarPolicy.AS_NEEDED);
private final ObjectProperty<ScrollBarPolicy> verticalScrollBarPolicyProperty = new SimpleObjectProperty<>(
ScrollBarPolicy.AS_NEEDED);
// contents
private Group contentGroup = new Group();
private ReadOnlyObjectWrapper<Affine> contentTransformProperty = new ReadOnlyObjectWrapper<>(
new Affine());
// content and scrollable bounds
private double[] contentBounds = new double[] { 0d, 0d, 0d, 0d };
private double[] scrollableBounds = new double[] { 0d, 0d, 0d, 0d };
private ObjectBinding<Bounds> contentBoundsBinding = new ObjectBinding<Bounds>() {
@Override
protected Bounds computeValue() {
return new BoundingBox(contentBounds[0], contentBounds[1],
contentBounds[2] - contentBounds[0],
contentBounds[3] - contentBounds[1]);
}
};
private ObjectBinding<Bounds> scrollableBoundsBinding = new ObjectBinding<Bounds>() {
@Override
protected Bounds computeValue() {
return new BoundingBox(scrollableBounds[0], scrollableBounds[1],
scrollableBounds[2] - scrollableBounds[0],
scrollableBounds[3] - scrollableBounds[1]);
}
};
private ReadOnlyObjectWrapper<Bounds> contentBoundsProperty = new ReadOnlyObjectWrapper<>();
private ReadOnlyObjectWrapper<Bounds> scrollableBoundsProperty = new ReadOnlyObjectWrapper<>();
// layers within the visualization
private Pane scrolledPane = new Pane();
private Group underlayGroup = new Group();
private Group scrolledUnderlayGroup = new Group();
private Group scrolledOverlayGroup = new Group();
private Group overlayGroup = new Group();
// Listener to update the scrollbars in response to Number changes (e.g.
// width and height).
private ChangeListener<Number> updateScrollBarsOnSizeChangeListener = new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable,
Number oldHeight, Number newHeight) {
updateScrollBars();
}
};
// Listener to update the scrollbars in response to Bounds changes (e.g.
// scrolled pane bounds and content group bounds).
private ChangeListener<Bounds> updateScrollBarsOnBoundsChangeListener = new ChangeListener<Bounds>() {
@Override
public void changed(ObservableValue<? extends Bounds> observable,
Bounds oldBounds, Bounds newBounds) {
updateScrollBars();
}
};
// Listener to update the scrollbars in response to ScrollBarPolicy
// changes.
private ChangeListener<ScrollBarPolicy> updateScrollBarsOnPolicyChangeListener = new ChangeListener<ScrollBarPolicy>() {
@Override
public void changed(
ObservableValue<? extends ScrollBarPolicy> observable,
ScrollBarPolicy oldValue, ScrollBarPolicy newValue) {
updateScrollBars();
}
};
private ChangeListener<Number> horizontalScrollBarValueChangeListener = new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable,
Number oldValue, Number newValue) {
if (horizontalScrollBar.isVisible()) {
getScrolledPane()
.setTranslateX(computeTx(newValue.doubleValue()));
}
}
};
private ChangeListener<Number> verticalScrollBarValueChangeListener = new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable,
Number oldValue, Number newValue) {
if (verticalScrollBar.isVisible()) {
getScrolledPane()
.setTranslateY(computeTy(newValue.doubleValue()));
}
}
};
/**
* Constructs a new {@link InfiniteCanvas}.
*/
public InfiniteCanvas() {
// bind bounds properties to predefined bindings
contentBoundsProperty.bind(contentBoundsBinding);
scrollableBoundsProperty.bind(scrollableBoundsBinding);
// create scrollbars
scrollBarGroup = createScrollBarGroup();
// create grid
grid = createGrid();
// initially set grid transform
updateGridTransform(gridTransformProperty.get());
// initially paint the tile image and use it for filling the
// background of this node
repaintGrid();
// create visualization
getChildren().addAll(createLayers());
getScrolledPane().getChildren().addAll(createScrolledLayers());
// add content transformation to content group
getContentGroup().getTransforms().add(getContentTransform());
// register listeners for updating the scrollbars
registerUpdateScrollBarsOnBoundsChanges();
registerUpdateScrollBarsOnSizeChanges();
registerUpdateScrollBarsOnPolicyChanges();
// enable the background grid
if (showGridProperty.get()) {
showGrid();
}
// register for "showGrid" changes to enable/disable the grid
showGridProperty.addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> observable,
Boolean oldValue, Boolean newValue) {
if (newValue.booleanValue()) {
showGrid();
} else {
hideGrid();
}
}
});
// enable grid zooming
if (showGridProperty.get()) {
zoomGrid();
}
// register for "zoomGrid" changes to enable/disable grid zooming
zoomGridProperty.addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> observable,
Boolean oldValue, Boolean newValue) {
if (newValue.booleanValue()) {
zoomGrid();
} else {
unzoomGrid();
}
}
});
// enable content clipping
if (clipContentProperty.get()) {
clipContent();
}
// register for "clipContent" changes to enable/disable content clipping
clipContentProperty.addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> observable,
Boolean oldValue, Boolean newValue) {
if (newValue.booleanValue()) {
clipContent();
} else {
unclipContent();
}
}
});
}
/**
* Enables content clipping for this {@link InfiniteCanvas}.
*/
protected void clipContent() {
clippingRectangle.widthProperty().bind(widthProperty());
clippingRectangle.heightProperty().bind(heightProperty());
setClip(clippingRectangle);
}
/**
* Returns the {@link BooleanProperty} that determines if this
* {@link InfiniteCanvas} does clipping, i.e. restricts its visibility to
* its {@link #layoutBoundsProperty()}.
*
* @return The {@link BooleanProperty} that determines if this
* {@link InfiniteCanvas} does clipping.
*/
public BooleanProperty clipContentProperty() {
return clipContentProperty;
}
/**
* Computes the bounds <code>[min-x, min-y, max-x, max-y]</code> surrounding
* the {@link #getContentGroup() content group} within the coordinate system
* of this {@link InfiniteCanvas}.
*
* @return The bounds <code>[min-x, min-y, max-x, max-y]</code> surrounding
* the {@link #getContentGroup() content group} within the
* coordinate system of this {@link InfiniteCanvas}.
*/
protected double[] computeContentBoundsInLocal() {
Bounds contentBoundsInScrolledPane = getContentGroup()
.getBoundsInParent();
double minX = contentBoundsInScrolledPane.getMinX();
double maxX = contentBoundsInScrolledPane.getMaxX();
double minY = contentBoundsInScrolledPane.getMinY();
double maxY = contentBoundsInScrolledPane.getMaxY();
Point2D minInScrolled = getScrolledPane().localToParent(minX, minY);
double realMinX = minInScrolled.getX();
double realMinY = minInScrolled.getY();
double realMaxX = realMinX + (maxX - minX);
double realMaxY = realMinY + (maxY - minY);
return new double[] { realMinX, realMinY, realMaxX, realMaxY };
}
/**
* Converts a horizontal translation distance into the corresponding
* horizontal scrollbar value.
*
* @param tx
* The horizontal translation distance.
* @return The horizontal scrollbar value corresponding to the given
* translation.
*/
protected double computeHv(double tx) {
return lerp(horizontalScrollBar.getMin(), horizontalScrollBar.getMax(),
norm(scrollableBounds[0], scrollableBounds[2] - getWidth(),
-tx));
}
/**
* Computes and returns the bounds of the scrollable area within this
* {@link InfiniteCanvas}.
*
* @return The bounds of the scrollable area, i.e.
* <code>[minx, miny, maxx, maxy]</code>.
*/
protected double[] computeScrollableBoundsInLocal() {
double[] cb = Arrays.copyOf(contentBounds, contentBounds.length);
Bounds db = getContentGroup().getBoundsInParent();
// factor in the viewport extending the content bounds
if (cb[0] < 0) {
cb[0] = 0;
}
if (cb[1] < 0) {
cb[1] = 0;
}
if (cb[2] > getWidth()) {
cb[2] = 0;
} else {
cb[2] = getWidth() - cb[2];
}
if (cb[3] > getHeight()) {
cb[3] = 0;
} else {
cb[3] = getHeight() - cb[3];
}
return new double[] { db.getMinX() - cb[0], db.getMinY() - cb[1],
db.getMaxX() + cb[2], db.getMaxY() + cb[3] };
}
/**
* Converts a horizontal scrollbar value into the corresponding horizontal
* translation distance.
*
* @param hv
* The horizontal scrollbar value.
* @return The horizontal translation distance corresponding to the given
* scrollbar value.
*/
protected double computeTx(double hv) {
return -lerp(scrollableBounds[0], scrollableBounds[2] - getWidth(),
norm(horizontalScrollBar.getMin(), horizontalScrollBar.getMax(),
hv));
}
/**
* Converts a vertical scrollbar value into the corresponding vertical
* translation distance.
*
* @param vv
* The vertical scrollbar value.
* @return The vertical translation distance corresponding to the given
* scrollbar value.
*/
protected double computeTy(double vv) {
return -lerp(scrollableBounds[1], scrollableBounds[3] - getHeight(),
norm(verticalScrollBar.getMin(), verticalScrollBar.getMax(),
vv));
}
/**
* Converts a vertical translation distance into the corresponding vertical
* scrollbar value.
*
* @param ty
* The vertical translation distance.
* @return The vertical scrollbar value corresponding to the given
* translation.
*/
protected double computeVv(double ty) {
return lerp(verticalScrollBar.getMin(), verticalScrollBar.getMax(),
norm(scrollableBounds[1], scrollableBounds[3] - getHeight(),
-ty));
}
/**
* Provides the visual bounds of the content group in the local coordinate
* system of this {@link InfiniteCanvas} as a (read-only) property.
*
* @return The bounds of the content group, i.e.
* <code>minx, miny, maxx, maxy</code> as
* {@link ReadOnlyObjectProperty}.
*/
public ReadOnlyObjectProperty<Bounds> contentBoundsProperty() {
return contentBoundsProperty.getReadOnlyProperty();
}
/**
* Returns the viewport transform as a (read-only) property.
*
* @return The viewport transform as {@link ReadOnlyObjectProperty}.
*/
public ReadOnlyObjectProperty<Affine> contentTransformProperty() {
return contentTransformProperty.getReadOnlyProperty();
}
/**
* Creates the {@link Region} that renders the grid (when it is enabled).
*
* @return The newly created {@link Region} that renders the grid.
*/
protected Region createGrid() {
Region grid = new Region();
grid.getTransforms().add(gridTransform);
// ensure the transformation matrix is up-to-date
gridTransformProperty.addListener(updateGridTransformListener);
// repaint the tile image in case the cell size changes
gridCellWidthProperty.addListener(repaintGridTileListener);
gridCellHeightProperty.addListener(repaintGridTileListener);
return grid;
}
/**
* Locate or create an {@link Image} that represents a single grid
* cell/tile. The {@link Image}'s dimensions is expected to match the grid
* cell size (width and height).
*
* @return An {@link Image} that represents a single grid cell/tile.
*/
protected Image createGridTile() {
// create a writable image for drawing a single grid cell
WritableImage gridTile = new WritableImage(gridCellWidthProperty.get(),
gridCellHeightProperty.get());
// draw the top left pixel in black (rest is transparent)
gridTile.getPixelWriter().setColor(0, 0, DEFAULT_GRID_POINT_COLOR);
return gridTile;
}
/**
* Returns a list containing the top level layers in the visualization of
* this {@link InfiniteCanvas}. Per default, the underlay group, the
* scrolled pane, the overlay group, and the scrollbar group are returned in
* that order.
*
* @return A list containing the top level layers in the visualization of
* this {@link InfiniteCanvas}.
*/
protected List<? extends Node> createLayers() {
return Arrays.asList(getUnderlayGroup(), getScrolledPane(),
getOverlayGroup(), getScrollBarGroup());
}
/**
* Creates the {@link Group} designated for holding the scrollbars and
* places the scrollbars in it. Furthermore, event listeners are registered
* to update the scroll offset upon scrollbar movement.
*
* @return The {@link Group} designated for holding the scrollbars.
*/
protected Group createScrollBarGroup() {
// create horizontal scrollbar
horizontalScrollBar = new ScrollBar();
horizontalScrollBar.setVisible(false);
horizontalScrollBar.setOpacity(0.5);
// create vertical scrollbar
verticalScrollBar = new ScrollBar();
verticalScrollBar.setOrientation(Orientation.VERTICAL);
verticalScrollBar.setVisible(false);
verticalScrollBar.setOpacity(0.5);
// bind horizontal size
DoubleBinding vWidth = new DoubleBinding() {
{
bind(verticalScrollBar.visibleProperty(),
verticalScrollBar.widthProperty());
}
@Override
protected double computeValue() {
return verticalScrollBar.isVisible()
? verticalScrollBar.getWidth() : 0;
}
};
horizontalScrollBar.prefWidthProperty()
.bind(widthProperty().subtract(vWidth));
// bind horizontal y position
horizontalScrollBar.layoutYProperty().bind(heightProperty()
.subtract(horizontalScrollBar.heightProperty()));
// bind vertical size
DoubleBinding hHeight = new DoubleBinding() {
{
bind(horizontalScrollBar.visibleProperty(),
horizontalScrollBar.heightProperty());
}
@Override
protected double computeValue() {
return horizontalScrollBar.isVisible()
? horizontalScrollBar.getHeight() : 0;
}
};
verticalScrollBar.prefHeightProperty()
.bind(heightProperty().subtract(hHeight));
// bind vertical x position
verticalScrollBar.layoutXProperty().bind(
widthProperty().subtract(verticalScrollBar.widthProperty()));
// fade in/out on mouse enter/exit
registerFadeInOutTransitions(horizontalScrollBar);
registerFadeInOutTransitions(verticalScrollBar);
horizontalScrollBar.valueProperty()
.addListener(horizontalScrollBarValueChangeListener);
verticalScrollBar.valueProperty()
.addListener(verticalScrollBarValueChangeListener);
return new Group(horizontalScrollBar, verticalScrollBar);
}
/**
* Returns a list containing the scrolled layers in the visualization of
* this {@link InfiniteCanvas}. Per default, the grid canvas, the scrolled
* underlay group, the content group, and the scrolled overlay group are
* returned in that order.
*
* @return A list containing the top level layers in the visualization of
* this {@link InfiniteCanvas}.
*/
protected List<? extends Node> createScrolledLayers() {
return Arrays.asList(getGridCanvas(), getScrolledUnderlayGroup(),
getContentGroup(), getScrolledOverlayGroup());
}
/**
* Adjusts the {@link #horizontalScrollOffsetProperty()}, the
* {@link #verticalScrollOffsetProperty()}, and the
* {@link #contentTransformProperty()}, so that the
* {@link #getContentGroup()} is fully visible within the bounds of this
* {@link InfiniteCanvas} if possible. The content will be centered, but the
* given <i>zoomMin</i> and <i>zoomMax</i> values restrict the zoom factor,
* so that the content might exceed the canvas, or does not fill it
* completely.
* <p>
* Note, that the {@link #contentTransformProperty()} is set to a pure scale
* transformation by this method.
* <p>
* Note, that fit-to-size cannot be performed in all situations. If the
* content area is 0 or the canvas area is 0, then this method cannot fit
* the content to the canvas size, and therefore, throws an
* {@link IllegalStateException}. The following condition can be used to
* test if fit-to-size can be performed:
*
* <pre>
* if (infiniteCanvas.getWidth() > 0 && infiniteCanvas.getHeight() > 0
* && infiniteCanvas.getContentBounds().getWidth() > 0
* && infiniteCanvas.getContentBounds().getHeight() > 0) {
* // save to call fit-to-size here
* infiniteCanvas.fitToSize();
* }
* </pre>
*
* @param zoomMin
* The minimum zoom level.
* @param zoomMax
* The maximum zoom level.
* @throws IllegalStateException
* when the content area is zero or the canvas area is zero.
*/
public void fitToSize(double zoomMin, double zoomMax) {
// validate content size is not 0
Bounds contentBounds = getContentBounds();
double contentWidth = contentBounds.getWidth();
if (Double.isNaN(contentWidth) || Double.isInfinite(contentWidth)
|| contentWidth <= 0) {
throw new IllegalStateException("Content area is zero.");
}
double contentHeight = contentBounds.getHeight();
if (Double.isNaN(contentHeight) || Double.isInfinite(contentHeight)
|| contentHeight <= 0) {
throw new IllegalStateException("Content area is zero.");
}
// validate canvas size is not 0
if (getWidth() <= 0 || getHeight() <= 0) {
throw new IllegalStateException("Canvas area is zero.");
}
// compute zoom factor
double zf = Math.min(getWidth() / contentWidth,
getHeight() / contentHeight);
// validate zoom factor
if (Double.isInfinite(zf) || Double.isNaN(zf) || zf <= 0) {
throw new IllegalStateException("Invalid zoom factor.");
}
// compute content center
double cx = contentBounds.getMinX() + contentBounds.getWidth() / 2;
double cy = contentBounds.getMinY() + contentBounds.getHeight() / 2;
// compute visible area center
double vx = getWidth() / 2;
double vy = getHeight() / 2;
// scroll to center position
setHorizontalScrollOffset(getHorizontalScrollOffset() + vx - cx);
setVerticalScrollOffset(getVerticalScrollOffset() + vy - cy);
// compute pivot point for zoom within content coordinates
Point2D pivot = getContentGroup().sceneToLocal(vx, vy);
// restrict zoom factor to [zoomMin, zoomMax] range
AffineTransform contentTransform = FX2Geometry
.toAffineTransform(getContentTransform());
double realZoomFactor = contentTransform.getScaleX() * zf;
if (realZoomFactor > zoomMax) {
zf = zoomMax / contentTransform.getScaleX();
}
if (realZoomFactor < zoomMin) {
zf = zoomMin / contentTransform.getScaleX();
}
// compute scale transformation (around visible center)
AffineTransform scaleTransform = new AffineTransform()
.translate(pivot.getX(), pivot.getY()).scale(zf, zf)
.translate(-pivot.getX(), -pivot.getY());
// concatenate old transformation and scale transformation to yield the
// new transformation
AffineTransform newTransform = contentTransform
.concatenate(scaleTransform);
setContentTransform(Geometry2FX.toFXAffine(newTransform));
}
/**
* Returns the value of the {@link #contentBoundsProperty()}.
*
* @return The value of the {@link #contentBoundsProperty()}.
*/
public Bounds getContentBounds() {
return contentBoundsProperty.get();
}
/**
* Returns the {@link Group} designated for holding the scrolled content.
*
* @return The {@link Group} designated for holding the scrolled content.
*/
public Group getContentGroup() {
return contentGroup;
}
/**
* Returns the transformation that is applied to the
* {@link #getContentGroup() content group}.
*
* @return The transformation that is applied to the
* {@link #getContentGroup() content group}.
*/
public Affine getContentTransform() {
return contentTransformProperty.get();
}
/**
* Returns the {@link Region} that is used to paint the background grid.
*
* @return The {@link Region} that is used to paint the background grid.
*/
protected Region getGridCanvas() {
return grid;
}
/**
* Returns the value of the {@link #gridCellHeightProperty()}.
*
* @return The value of the {@link #gridCellHeightProperty()}.
*/
public double getGridCellHeight() {
return gridCellHeightProperty.get();
}
/**
* Returns the value of the {@link #gridCellWidthProperty()}.
*
* @return The value of the {@link #gridCellWidthProperty()}.
*/
public double getGridCellWidth() {
return gridCellWidthProperty.get();
}
/**
* Returns the horizontal {@link ScrollBar}, or <code>null</code> if the
* horizontal {@link ScrollBar} was not yet created.
*
* @return The horizontal {@link ScrollBar}.
*/
public ScrollBar getHorizontalScrollBar() {
return horizontalScrollBar;
}
/**
* Returns the {@link ScrollBarPolicy} that is currently used to decide when
* to show a horizontal scrollbar.
*
* @return The {@link ScrollBarPolicy} that is currently used to decide when
* to show a horizontal scrollbar.
*/
public ScrollBarPolicy getHorizontalScrollBarPolicy() {
return horizontalScrollBarPolicyProperty.get();
}
/**
* Returns the current horizontal scroll offset.
*
* @return The current horizontal scroll offset.
*/
public double getHorizontalScrollOffset() {
return getScrolledPane().getTranslateX();
}
/**
* Returns the overlay {@link Group} that is rendered above the contents but
* below the scrollbars.
*
* @return The overlay {@link Group} that is rendered above the contents but
* below the scrollbars.
*/
public Group getOverlayGroup() {
return overlayGroup;
}
/**
* Returns the value of the {@link #scrollableBoundsProperty()}.
*
* @return The value of the {@link #scrollableBoundsProperty()}.
*/
public Bounds getScrollableBounds() {
return scrollableBoundsProperty.get();
}
/**
* Returns the {@link Group} designated for holding the {@link ScrollBar}s.
*
* @return The {@link Group} designated for holding the {@link ScrollBar}s.
*/
protected Group getScrollBarGroup() {
return scrollBarGroup;
}
/**
* Returns the scrolled overlay {@link Group}.
*
* @return The scrolled overlay {@link Group}.
*/
public Group getScrolledOverlayGroup() {
return scrolledOverlayGroup;
}
/**
* Returns the {@link Pane} which is translated when scrolling. This
* {@link Pane} contains the {@link #getContentGroup()}, therefore, the
* {@link #getContentTransform()} does not influence the scroll offset.
*
* @return The {@link Pane} that is translated when scrolling.
*/
protected Pane getScrolledPane() {
return scrolledPane;
}
/**
* Returns the scrolled underlay {@link Group}.
*
* @return The scrolled underlay {@link Group}.
*/
public Group getScrolledUnderlayGroup() {
return scrolledUnderlayGroup;
}
/**
* Returns the underlay {@link Group}.
*
* @return The underlay {@link Group}.
*/
public Group getUnderlayGroup() {
return underlayGroup;
}
/**
* Returns the vertical {@link ScrollBar}, or <code>null</code> if the
* vertical {@link ScrollBar} was not yet created.
*
* @return The vertical {@link ScrollBar}.
*/
public ScrollBar getVerticalScrollBar() {
return verticalScrollBar;
}
/**
* Returns the {@link ScrollBarPolicy} that is currently used to decide when
* to show a vertical scrollbar.
*
* @return The {@link ScrollBarPolicy} that is currently used to decide when
* to show a vertical scrollbar.
*/
public ScrollBarPolicy getVerticalScrollBarPolicy() {
return verticalScrollBarPolicyProperty.get();
}
/**
* Returns the current vertical scroll offset.
*
* @return The current vertical scroll offset.
*/
public double getVerticalScrollOffset() {
return getScrolledPane().getTranslateY();
}
/**
* Returns the grid cell height as a (writable) property.
*
* @return The grid cell height as a {@link DoubleProperty}.
*/
public IntegerProperty gridCellHeightProperty() {
return gridCellHeightProperty;
}
/**
* Returns the grid cell width as a (writable) property.
*
* @return The grid cell width as a {@link DoubleProperty}.
*/
public IntegerProperty gridCellWidthProperty() {
return gridCellWidthProperty;
}
/**
* Disables the background grid.
*/
protected void hideGrid() {
grid.setVisible(false);
grid.layoutXProperty().unbind();
grid.layoutYProperty().unbind();
grid.prefWidthProperty().unbind();
grid.prefHeightProperty().unbind();
}
/**
* Returns the {@link ObjectProperty} that controls the
* {@link ScrollBarPolicy} that decides when to show a horizontal scrollbar.
*
* @return The {@link ObjectProperty} that controls the
* {@link ScrollBarPolicy} that decides when to show a horizontal
* scrollbar.
*/
public ObjectProperty<ScrollBarPolicy> horizontalScrollBarPolicyProperty() {
return horizontalScrollBarPolicyProperty;
}
/**
* Returns the horizontal scroll offset as a property.
*
* @return A {@link DoubleProperty} representing the horizontal scroll
* offset.
*/
public DoubleProperty horizontalScrollOffsetProperty() {
return getScrolledPane().translateXProperty();
}
/**
* Returns the value of the {@link #clipContentProperty()}.
*
* @return The value of the {@link #clipContentProperty()}.
*/
public boolean isClipContent() {
return clipContentProperty.get();
}
/**
* Returns the value of the {@link #showGridProperty()}.
*
* @return The value of the {@link #showGridProperty()}.
*/
public boolean isShowGrid() {
return showGridProperty.get();
}
/**
* Returns the value of the {@link #zoomGridProperty()}.
*
* @return The value of the {@link #zoomGridProperty()}.
*/
public boolean isZoomGrid() {
return zoomGridProperty.get();
}
/**
* Linear interpolation between <i>min</i> and <i>max</i> at the given
* <i>ratio</i>. Returns the interpolated value in the interval
* <code>[min;max]</code>.
*
* @param min
* The lower interval bound.
* @param max
* The upper interval bound.
* @param ratio
* A value in the interval <code>[0;1]</code>.
* @return The interpolated value.
*/
protected double lerp(double min, double max, double ratio) {
double d = (1 - ratio) * min + ratio * max;
return Double.isNaN(d) ? 0 : Math.min(max, Math.max(min, d));
}
/**
* Normalizes a given <i>value</i> which is in range <code>[min;max]</code>
* to range <code>[0;1]</code>.
*
* @param min
* The lower bound of the range.
* @param max
* The upper bound of the range.
* @param value
* The value in the range.
* @return The normalized value (in range <code>[0;1]</code>).
*/
protected double norm(double min, double max, double value) {
double d = (value - min) / (max - min);
return Double.isNaN(d) ? 0 : Math.min(1, Math.max(0, d));
}
/**
* Registers fade in/out transitions for the given {@link Node}. The
* transitions are used when the mouse enters/exits the node.
*
* @param node
* The {@link Node} to which fade in/out transitions are added
* upon mouse enter/exit.
*/
protected void registerFadeInOutTransitions(final Node node) {
// create transitions
final FadeTransition fadeInTransition = new FadeTransition(
Duration.millis(200), node);
fadeInTransition.setToValue(1.0);
final FadeTransition fadeOutTransition = new FadeTransition(
Duration.millis(200), node);
fadeOutTransition.setToValue(0.5);
// create actions
final Runnable fadeIn = new Runnable() {
@Override
public void run() {
fadeOutTransition.stop();
fadeInTransition.playFromStart();
}
};
final Runnable fadeOut = new Runnable() {
@Override
public void run() {
fadeInTransition.stop();
fadeOutTransition.playFromStart();
}
};
// register event handlers
node.setOnMouseEntered(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
fadeIn.run();
}
});
node.setOnMouseExited(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
if (!node.isPressed()) {
fadeOut.run();
}
}
});
node.pressedProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> observable,
Boolean oldValue, Boolean newValue) {
if (oldValue.booleanValue() && !newValue.booleanValue()) {
fadeOut.run();
}
}
});
}
/**
* Registers listeners on the bounds-in-local property of the
* {@link #getScrolledPane()} and on the bounds-in-parent property of the
* {@link #getContentGroup()} that will call {@link #updateScrollBars()}
* when one of the bounds is changed.
*/
protected void registerUpdateScrollBarsOnBoundsChanges() {
getScrolledPane().boundsInParentProperty()
.addListener(updateScrollBarsOnBoundsChangeListener);
getContentGroup().boundsInParentProperty()
.addListener(updateScrollBarsOnBoundsChangeListener);
}
/**
* Registers listeners on the {@link #horizontalScrollBarPolicyProperty()}
* and on the {@link #verticalScrollBarPolicyProperty()} that will call
* {@link #updateScrollBars()} when one of the {@link ScrollBarPolicy}s
* changes.
*/
protected void registerUpdateScrollBarsOnPolicyChanges() {
horizontalScrollBarPolicyProperty
.addListener(updateScrollBarsOnPolicyChangeListener);
verticalScrollBarPolicyProperty
.addListener(updateScrollBarsOnPolicyChangeListener);
}
/**
* Registers listeners on the {@link #widthProperty()} and on the
* {@link #heightProperty()} that will call {@link #updateScrollBars()} when
* the size of this {@link InfiniteCanvas} changes.
*/
protected void registerUpdateScrollBarsOnSizeChanges() {
widthProperty().addListener(updateScrollBarsOnSizeChangeListener);
heightProperty().addListener(updateScrollBarsOnSizeChangeListener);
}
/**
* Repaints the tile image that depends on the grid cell size only. The tile
* image is repeated when repainting the grid.
*/
protected void repaintGrid() {
Image tile = createGridTile();
// create a background fill for this node from the tile image
BackgroundPosition backgroundPosition = new BackgroundPosition(
Side.LEFT, 0, false, Side.TOP, 0, false);
BackgroundImage backgroundImage = new BackgroundImage(tile,
BackgroundRepeat.REPEAT, BackgroundRepeat.REPEAT,
backgroundPosition, BackgroundSize.DEFAULT);
Background background = new Background(backgroundImage);
// apply that background fill
grid.setBackground(background);
}
/**
* Ensures that the specified child {@link Node} is visible to the user by
* scrolling to its position. The effect and style of the node are taken
* into consideration. After revealing a node, it will be fully visible if
* it fits within the current viewport bounds.
* <p>
* When the child node's left side is left to the viewport, it will touch
* the left border of the viewport after revealing. When the child node's
* right side is right to the viewport, it will touch the right border of
* the viewport after revealing. When the child node's top side is above the
* viewport, it will touch the top border of the viewport after revealing.
* When the child node's bottom side is below the viewport, it will touch
* the bottom border of the viewport after revealing.
* <p>
* The top and left sides have preference over the bottom and right sides,
* i.e. when the top side is aligned with the viewport, the bottom side will
* not be aligned, and when the left side is aligned with the viewport, the
* right side will not be aligned.
*
* @param child
* The child {@link Node} to reveal.
*/
public void reveal(Node child) {
Bounds bounds = sceneToLocal(
child.localToScene(child.getBoundsInLocal()));
if (bounds.getHeight() <= getHeight()) {
if (bounds.getMinY() < 0) {
setVerticalScrollOffset(
getVerticalScrollOffset() - bounds.getMinY());
} else if (bounds.getMaxY() > getHeight()) {
setVerticalScrollOffset(getVerticalScrollOffset() + getHeight()
- bounds.getMaxY());
}
}
if (bounds.getWidth() <= getWidth()) {
if (bounds.getMinX() < 0) {
setHorizontalScrollOffset(
getHorizontalScrollOffset() - bounds.getMinX());
} else if (bounds.getMaxX() > getWidth()) {
setHorizontalScrollOffset(getHorizontalScrollOffset()
+ getWidth() - bounds.getMaxX());
}
}
}
/**
* Returns the bounds of the scrollable area in local coordinates of this
* {@link InfiniteCanvas} as a (read-only) property. The scrollable area
* corresponds to the visual bounds of the content group, which is expanded
* to cover at least the area of this {@link InfiniteCanvas} (i.e. the
* viewport) if necessary. It is thereby also the area that can be navigated
* via the scroll bars.
*
* @return The bounds of the scrollable area, i.e.
* <code>minx, miny, maxx, maxy</code> as
* {@link ReadOnlyObjectProperty}.
*/
public ReadOnlyObjectProperty<Bounds> scrollableBoundsProperty() {
return scrollableBoundsProperty.getReadOnlyProperty();
}
/**
* Sets the value of the {@link #clipContentProperty()} to the given value.
*
* @param clipContent
* The new value for the {@link #clipContentProperty()}.
*/
public void setClipContent(boolean clipContent) {
clipContentProperty.set(clipContent);
}
/**
* Sets the transformation matrix of the {@link #getContentTransform()
* viewport transform} to the values specified by the given {@link Affine}.
*
* @param tx
* The {@link Affine} determining the new
* {@link #getContentTransform() viewport transform}.
*/
public void setContentTransform(Affine tx) {
Affine viewportTransform = contentTransformProperty.get();
// Unregister bounds listeners so that transformation changes do not
// cause updates. Use flag to be aware if the transformation changed.
unregisterUpdateScrollBarsOnBoundsChanges();
boolean valuesChanged = false;
if (viewportTransform.getMxx() != tx.getMxx()) {
viewportTransform.setMxx(tx.getMxx());
valuesChanged = true;
}
if (viewportTransform.getMxy() != tx.getMxy()) {
viewportTransform.setMxy(tx.getMxy());
valuesChanged = true;
}
if (viewportTransform.getMyx() != tx.getMyx()) {
viewportTransform.setMyx(tx.getMyx());
valuesChanged = true;
}
if (viewportTransform.getMyy() != tx.getMyy()) {
viewportTransform.setMyy(tx.getMyy());
valuesChanged = true;
}
if (viewportTransform.getTx() != tx.getTx()) {
viewportTransform.setTx(tx.getTx());
valuesChanged = true;
}
if (viewportTransform.getTy() != tx.getTy()) {
viewportTransform.setTy(tx.getTy());
valuesChanged = true;
}
// Update scrollbars if the transformation changed.
if (valuesChanged) {
updateScrollBars();
}
// Register previously unregistered listeners.
registerUpdateScrollBarsOnBoundsChanges();
}
/**
* Assigns the given value to the {@link #gridCellHeightProperty()}.
*
* @param gridCellHeight
* The grid cell height that is assigned to the
* {@link #gridCellHeightProperty()}.
*/
public void setGridCellHeight(int gridCellHeight) {
gridCellHeightProperty.set(gridCellHeight);
}
/**
* Assigns the given value to the {@link #gridCellWidthProperty()}.
*
* @param gridCellWidth
* The grid cell width that is assigned to the
* {@link #gridCellWidthProperty()}.
*/
public void setGridCellWidth(int gridCellWidth) {
gridCellWidthProperty.set(gridCellWidth);
}
/**
* Sets the value of the {@link #horizontalScrollBarPolicyProperty()} to the
* given {@link ScrollBarPolicy}.
*
* @param horizontalScrollBarPolicy
* The new {@link ScrollBarPolicy} for the horizontal scrollbar.
*/
public void setHorizontalScrollBarPolicy(
ScrollBarPolicy horizontalScrollBarPolicy) {
horizontalScrollBarPolicyProperty.set(horizontalScrollBarPolicy);
}
/**
* Sets the horizontal scroll offset to the given value.
*
* @param scrollOffsetX
* The new horizontal scroll offset.
*/
public void setHorizontalScrollOffset(double scrollOffsetX) {
getScrolledPane().setTranslateX(scrollOffsetX);
}
/**
* Assigns the given value to the {@link #showGridProperty()}.
*
* @param showGrid
* The new value that is assigned to the
* {@link #showGridProperty()}.
*/
public void setShowGrid(boolean showGrid) {
showGridProperty.set(showGrid);
}
/**
* Sets the value of the {@link #verticalScrollBarPolicyProperty()} to the
* given {@link ScrollBarPolicy}.
*
* @param verticalScrollBarPolicy
* The new {@link ScrollBarPolicy} for the vertical scrollbar.
*/
public void setVerticalScrollBarPolicy(
ScrollBarPolicy verticalScrollBarPolicy) {
verticalScrollBarPolicyProperty.set(verticalScrollBarPolicy);
}
/**
* Sets the vertical scroll offset to the given value.
*
* @param scrollOffsetY
* The new vertical scroll offset.
*/
public void setVerticalScrollOffset(double scrollOffsetY) {
getScrolledPane().setTranslateY(scrollOffsetY);
}
/**
* Assigns the given value to the {@link #showGridProperty()}.
*
* @param zoomGrid
* The new value that is assigned to the
* {@link #showGridProperty()}.
*/
public void setZoomGrid(boolean zoomGrid) {
zoomGridProperty.set(zoomGrid);
}
/**
* Enables the background grid.
*/
protected void showGrid() {
grid.setVisible(true);
grid.layoutXProperty().bind(new DoubleBinding() {
{
super.bind(gridTransformProperty.get().txProperty());
super.bind(gridTransformProperty.get().mxxProperty());
super.bind(scrollableBoundsProperty);
}
@Override
protected double computeValue() {
// get horizontal scroll offset
double minXInInfCanvas = scrollableBoundsProperty.get()
.getMinX();
// compute scaled grid cell width
Affine gridTransform = gridTransformProperty.get();
double mxx = gridTransform.getMxx();
double gridCellWidth = getGridCellWidth() * mxx;
// subtract content translation to compute horizontal offset
double correctedMinX = minXInInfCanvas - gridTransform.getTx();
// compute number of grid cell widths that fit into the
// horizontal offset
int gridCellOffsetCount = (int) (correctedMinX / gridCellWidth);
// XXX: Subtract -0.5 * scaleX so that the center of the first
// grid point is exactly at 0, 0 within the content layer
return (gridCellOffsetCount - 1) * gridCellWidth - 0.5 * mxx;
}
});
grid.layoutYProperty().bind(new DoubleBinding() {
{
super.bind(gridTransformProperty.get().tyProperty());
super.bind(gridTransformProperty.get().myyProperty());
super.bind(scrollableBoundsProperty);
}
@Override
protected double computeValue() {
// get vertical scroll offset
double minYInInfCanvas = scrollableBoundsProperty.get()
.getMinY();
// compute scaled grid cell height
Affine gridTransform = gridTransformProperty.get();
double myy = gridTransform.getMyy();
double gridCellHeight = getGridCellHeight() * myy;
// subtract content translation to compute vertical offset
double correctedMinY = minYInInfCanvas - gridTransform.getTy();
// compute number of grid cell heights that fit into the
// vertical offset
int gridCellOffsetCount = (int) (correctedMinY
/ gridCellHeight);
// XXX: Subtract -0.5 * scaleY so that the center of the first
// grid point is exactly at 0, 0 within the content layer
return (gridCellOffsetCount - 1) * gridCellHeight - 0.5 * myy;
}
});
grid.prefWidthProperty().bind(new DoubleBinding() {
{
super.bind(gridTransformProperty.get().mxxProperty());
super.bind(scrollableBoundsProperty);
}
@Override
protected double computeValue() {
if (scrollableBoundsProperty.get() == null) {
return 0;
}
return (scrollableBoundsProperty.get().getWidth())
/ gridTransformProperty.get().getMxx()
+ getGridCellWidth() * 2;
}
});
grid.prefHeightProperty().bind(new DoubleBinding() {
{
super.bind(gridTransformProperty.get().myyProperty());
super.bind(scrollableBoundsProperty);
}
@Override
protected double computeValue() {
if (scrollableBoundsProperty.get() == null) {
return 0;
}
return (scrollableBoundsProperty.get().getHeight())
/ gridTransformProperty.get().getMyy()
+ getGridCellHeight() * 2;
}
});
}
/**
* Returns the {@link BooleanProperty} that determines if a background grid
* is shown within this {@link InfiniteCanvas}.
*
* @return The {@link BooleanProperty} that determines if a background grid
* is shown within this {@link InfiniteCanvas}.
*/
public BooleanProperty showGridProperty() {
return showGridProperty;
}
/**
* Disables content clipping for this {@link InfiniteCanvas}.
*/
protected void unclipContent() {
clippingRectangle.widthProperty().unbind();
clippingRectangle.heightProperty().unbind();
setClip(null);
}
/**
* Unregisters the listeners that were previously registered within
* {@link #registerUpdateScrollBarsOnBoundsChanges()}.
*/
protected void unregisterUpdateScrollBarsOnBoundsChanges() {
getScrolledPane().boundsInParentProperty()
.removeListener(updateScrollBarsOnBoundsChangeListener);
getContentGroup().boundsInParentProperty()
.removeListener(updateScrollBarsOnBoundsChangeListener);
}
/**
* Disables zooming of the background grid.
*
* @see #zoomGrid()
* @see #zoomGridProperty()
*/
protected void unzoomGrid() {
Affine gridTransform = gridTransformProperty.get();
gridTransform.mxxProperty().unbind();
gridTransform.mxyProperty().unbind();
gridTransform.myxProperty().unbind();
gridTransform.myyProperty().unbind();
gridTransform.txProperty().unbind();
gridTransform.tyProperty().unbind();
}
/**
* This method is called when the grid transformation should be updated to
* match the given {@link Affine}. The grid transformation is
*
* @param transform
* The new transformation matrix for the grid canvas.
*/
protected void updateGridTransform(Affine transform) {
gridTransform.mxxProperty().bind(transform.mxxProperty());
gridTransform.mxyProperty().bind(transform.mxyProperty());
gridTransform.myyProperty().bind(transform.myyProperty());
gridTransform.myxProperty().bind(transform.myxProperty());
gridTransform.txProperty().bind(transform.txProperty());
gridTransform.tyProperty().bind(transform.tyProperty());
}
/**
* Updates the {@link ScrollBar}s' visibilities, value ranges and value
* increments based on the {@link #computeContentBoundsInLocal() content
* bounds} and the {@link #computeScrollableBoundsInLocal() scrollable
* bounds}. The update is not done if any of the {@link ScrollBar}s is
* currently in use.
*/
protected void updateScrollBars() {
// do not update while a scrollbar is pressed, so that the scrollable
// area does not change while using a scrollbar
if (horizontalScrollBar.isPressed() || verticalScrollBar.isPressed()) {
return;
}
// determine current content bounds
double[] oldContentBounds = Arrays.copyOf(contentBounds,
contentBounds.length);
contentBounds = computeContentBoundsInLocal();
if (!Arrays.equals(oldContentBounds, contentBounds)) {
contentBoundsBinding.invalidate();
}
// show/hide horizontal scrollbar
ScrollBarPolicy hbarPolicy = horizontalScrollBarPolicyProperty.get();
boolean hbarIsNeeded = contentBounds[0] < -0.01
|| contentBounds[2] > getWidth() + 0.01;
if (hbarPolicy.equals(ScrollBarPolicy.ALWAYS)
|| hbarPolicy.equals(ScrollBarPolicy.AS_NEEDED)
&& hbarIsNeeded) {
horizontalScrollBar.setVisible(true);
} else {
horizontalScrollBar.setVisible(false);
}
// show/hide vertical scrollbar
ScrollBarPolicy vbarPolicy = verticalScrollBarPolicyProperty.get();
boolean vbarIsNeeded = contentBounds[1] < -0.01
|| contentBounds[3] > getHeight() + 0.01;
if (vbarPolicy.equals(ScrollBarPolicy.ALWAYS)
|| vbarPolicy.equals(ScrollBarPolicy.AS_NEEDED)
&& vbarIsNeeded) {
verticalScrollBar.setVisible(true);
} else {
verticalScrollBar.setVisible(false);
}
// determine current scrollable bounds
double[] oldScrollableBounds = Arrays.copyOf(scrollableBounds,
scrollableBounds.length);
scrollableBounds = computeScrollableBoundsInLocal();
if (!Arrays.equals(oldScrollableBounds, scrollableBounds)) {
scrollableBoundsBinding.invalidate();
}
// update scrollbar ranges
horizontalScrollBar.setMin(scrollableBounds[0]);
horizontalScrollBar.setMax(scrollableBounds[2]);
horizontalScrollBar.setVisibleAmount(getWidth());
horizontalScrollBar.setBlockIncrement(getWidth() / 2);
horizontalScrollBar.setUnitIncrement(getWidth() / 10);
verticalScrollBar.setMin(scrollableBounds[1]);
verticalScrollBar.setMax(scrollableBounds[3]);
verticalScrollBar.setVisibleAmount(getHeight());
verticalScrollBar.setBlockIncrement(getHeight() / 2);
verticalScrollBar.setUnitIncrement(getHeight() / 10);
// compute scrollbar values from canvas translation (in case the
// scrollbar values are incorrect)
// XXX: Remove scroll bar value listeners when adapting the values to
// prevent infinite recursion.
horizontalScrollBar.valueProperty()
.removeListener(horizontalScrollBarValueChangeListener);
verticalScrollBar.valueProperty()
.removeListener(verticalScrollBarValueChangeListener);
horizontalScrollBar
.setValue(computeHv(getScrolledPane().getTranslateX()));
verticalScrollBar
.setValue(computeVv(getScrolledPane().getTranslateY()));
horizontalScrollBar.valueProperty()
.addListener(horizontalScrollBarValueChangeListener);
verticalScrollBar.valueProperty()
.addListener(verticalScrollBarValueChangeListener);
}
/**
* Returns the {@link ObjectProperty} that controls the
* {@link ScrollBarPolicy} that decides when to show a vertical scrollbar.
*
* @return The {@link ObjectProperty} that controls the
* {@link ScrollBarPolicy} that decides when to show a vertical
* scrollbar.
*/
public ObjectProperty<ScrollBarPolicy> verticalScrollBarPolicyProperty() {
return verticalScrollBarPolicyProperty;
}
/**
* Returns the vertical scroll offset as a property.
*
* @return A {@link DoubleProperty} representing the vertical scroll offset.
*/
public DoubleProperty verticalScrollOffsetProperty() {
return getScrolledPane().translateYProperty();
}
/**
* Enables zooming of the background grid when the contents are zoomed.
*/
protected void zoomGrid() {
Affine gridTransform = gridTransformProperty.get();
Affine contentTransform = getContentTransform();
gridTransform.mxxProperty().bind(contentTransform.mxxProperty());
gridTransform.mxyProperty().bind(contentTransform.mxyProperty());
gridTransform.myxProperty().bind(contentTransform.myxProperty());
gridTransform.myyProperty().bind(contentTransform.myyProperty());
gridTransform.txProperty().bind(contentTransform.txProperty());
gridTransform.tyProperty().bind(contentTransform.tyProperty());
}
/**
* Returns the {@link BooleanProperty} that determines if the background
* grid is zoomed when the contents are zoomed.
*
* @return The {@link BooleanProperty} that determines if the background
* grid is zoomed when the contents are zoomed.
*/
public BooleanProperty zoomGridProperty() {
return zoomGridProperty;
}
}