/******************************************************************************* * Copyright (c) 2017 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: * Matthias Wienand (itemis AG) - initial API and implementation * *******************************************************************************/ package org.eclipse.gef.mvc.fx.parts; import java.util.Set; import org.eclipse.gef.fx.nodes.InfiniteCanvas; import org.eclipse.gef.geometry.convert.fx.FX2Geometry; import org.eclipse.gef.geometry.convert.fx.Geometry2FX; import org.eclipse.gef.mvc.fx.models.SnappingModel.SnappingLocation; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.geometry.Bounds; import javafx.geometry.Orientation; import javafx.geometry.Point2D; import javafx.scene.Node; import javafx.scene.paint.Color; import javafx.scene.shape.Line; import javafx.scene.shape.StrokeLineCap; import javafx.scene.shape.StrokeType; /** * The {@link SnappingLocationFeedbackPart} visualizes a * {@link SnappingLocation} by drawing a red line at the * {@link SnappingLocation} through the whole viewport. */ public class SnappingLocationFeedbackPart extends AbstractFeedbackPart<Line> { private SnappingLocation snappingLocation = null; private ChangeListener<? super Number> viewportSizeObserver = new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { onViewportSizeChanged(); } }; private ChangeListener<? super Number> viewportTranslationObserver = new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { onViewportTranslationChanged(); } }; @Override protected void doAttachToAnchorageVisual( IVisualPart<? extends Node> anchorage, String role) { super.doAttachToAnchorageVisual(anchorage, role); if (getAnchoragesUnmodifiable().size() == 1) { // add listeners InfiniteCanvas canvas = (InfiniteCanvas) anchorage.getRoot() .getViewer().getCanvas(); canvas.widthProperty().addListener(viewportSizeObserver); canvas.heightProperty().addListener(viewportSizeObserver); canvas.horizontalScrollOffsetProperty() .addListener(viewportTranslationObserver); canvas.verticalScrollOffsetProperty() .addListener(viewportTranslationObserver); } } @Override protected Line doCreateVisual() { Line line = new Line(); line.setStroke(Color.RED); line.setStrokeWidth(1); line.setStrokeType(StrokeType.CENTERED); line.setStrokeLineCap(StrokeLineCap.BUTT); line.setVisible(false); return line; } @Override protected void doDetachFromAnchorageVisual( IVisualPart<? extends Node> anchorage, String role) { super.doDetachFromAnchorageVisual(anchorage, role); if (getAnchoragesUnmodifiable().isEmpty()) { // remove listeners InfiniteCanvas canvas = (InfiniteCanvas) anchorage.getRoot() .getViewer().getCanvas(); canvas.widthProperty().removeListener(viewportSizeObserver); canvas.heightProperty().removeListener(viewportSizeObserver); canvas.horizontalScrollOffsetProperty() .removeListener(viewportTranslationObserver); canvas.verticalScrollOffsetProperty() .removeListener(viewportTranslationObserver); } } @Override protected void doRefreshVisual(final Line visual) { Set<IVisualPart<? extends Node>> anchorages = getAnchoragesUnmodifiable() .keySet(); if (anchorages.isEmpty()) { return; } IVisualPart<? extends Node> firstAnchorage = anchorages.iterator() .next(); if (!(firstAnchorage instanceof IContentPart)) { throw new IllegalStateException( "SnapToLocationFeedbackPart can only be attached to IContentPart."); } // host: the context part for which feedback is rendered IContentPart<? extends Node> host = (IContentPart<? extends Node>) firstAnchorage; // determine scrollable bounds in scene InfiniteCanvas canvas = (InfiniteCanvas) host.getRoot().getViewer() .getCanvas(); final Bounds canvasBoundsInScene = canvas .localToScene(canvas.getLayoutBounds()); // hide visual canvas.widthProperty().removeListener(viewportSizeObserver); canvas.heightProperty().removeListener(viewportSizeObserver); canvas.horizontalScrollOffsetProperty() .removeListener(viewportTranslationObserver); canvas.verticalScrollOffsetProperty() .removeListener(viewportTranslationObserver); visual.setVisible(false); canvas.horizontalScrollOffsetProperty() .addListener(viewportTranslationObserver); canvas.verticalScrollOffsetProperty() .addListener(viewportTranslationObserver); canvas.widthProperty().addListener(viewportSizeObserver); canvas.heightProperty().addListener(viewportSizeObserver); // update visual if (getSnappingLocation().getOrientation() == Orientation.VERTICAL) { // x location saved in snapping location double xInScene = getSnappingLocation().getPositionInScene(); // transform to local coordinates // XXX: an offset is added/subtracted from the scrollable bounds // min/max locations so that the feedback does not change the // scrollable bounds (which prevents a StackOverflowError) Point2D startLocal = visual.sceneToLocal(xInScene, canvasBoundsInScene.getMinY() + 3); Point2D endLocal = visual.sceneToLocal(xInScene, canvasBoundsInScene.getMaxY() - 3); // ensure pixel accuracy double xLocal = Math.floor(startLocal.getX()) + 0.5; visual.setStartX(xLocal); visual.setStartY(Math.floor(startLocal.getY() + 1) + 0.5); visual.setEndX(xLocal); visual.setEndY(Math.floor(endLocal.getY() - 1) - 0.5); } else { // y location saved in snapping location double yInScene = getSnappingLocation().getPositionInScene(); // transform to local coordinates // XXX: an offset is added/subtracted from the scrollable bounds // min/max locations so that the feedback does not change the // scrollable bounds (which prevents a StackOverflowError) Point2D startLocal = visual .sceneToLocal(canvasBoundsInScene.getMinX() + 3, yInScene); Point2D endLocal = visual .sceneToLocal(canvasBoundsInScene.getMaxX() - 3, yInScene); // ensure pixel accuracy double yLocal = Math.floor(startLocal.getY()) + 0.5; visual.setStartX(Math.floor(startLocal.getX() + 1) + 0.5); visual.setStartY(yLocal); visual.setEndX(Math.floor(endLocal.getX() - 1) - 0.5); visual.setEndY(yLocal); } // XXX: ensure visual is inside scrollable bounds to prevent // a StackOverflowError (bounds change => refresh feedback // => bounds change => refresh feedback => ...) Bounds visualBoundsInScene = visual.getParent() .localToScene(visual.getBoundsInParent()); visualBoundsInScene = Geometry2FX.toFXBounds( FX2Geometry.toRectangle(visualBoundsInScene).expand(2, 2)); if (canvasBoundsInScene.contains(visualBoundsInScene)) { // show visual canvas.widthProperty().removeListener(viewportSizeObserver); canvas.heightProperty().removeListener(viewportSizeObserver); canvas.horizontalScrollOffsetProperty() .removeListener(viewportTranslationObserver); canvas.verticalScrollOffsetProperty() .removeListener(viewportTranslationObserver); visual.setVisible(true); canvas.horizontalScrollOffsetProperty() .addListener(viewportTranslationObserver); canvas.verticalScrollOffsetProperty() .addListener(viewportTranslationObserver); canvas.widthProperty().addListener(viewportSizeObserver); canvas.heightProperty().addListener(viewportSizeObserver); } // else { // System.err.println( // "[ERROR] Cannot show snapping feedback because it exceeds the // scrollable bounds."); // } } /** * Returns the {@link SnappingLocation} for which feedback is visualized by * this {@link SnappingLocationFeedbackPart}. * * @return The {@link SnappingLocation} for which feedback is visualized by * this {@link SnappingLocationFeedbackPart}. */ public SnappingLocation getSnappingLocation() { return snappingLocation; } /** * Callback method that is invoked when the viewport size is changed. */ protected void onViewportSizeChanged() { refreshVisual(); } /** * Callback method that is invoked when the viewport translation (scrolling) * is changed. */ protected void onViewportTranslationChanged() { refreshVisual(); } /** * Sets the {@link SnappingLocation} for this feedback part to the given * value. * * @param snappingLocation * The new {@link SnappingLocation} for this feedback part. */ public void setSnappingLocation(SnappingLocation snappingLocation) { this.snappingLocation = snappingLocation; } }