/******************************************************************************* * 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: * Matthias Wienand (itemis AG) - initial API and implementation * Alexander Nyßen (itemis AG) - Fixes related to bug #437076 * *******************************************************************************/ package org.eclipse.gef.mvc.fx.handlers; import java.util.Comparator; import java.util.List; import org.eclipse.gef.fx.anchors.IAnchor; import org.eclipse.gef.fx.nodes.Connection; import org.eclipse.gef.fx.nodes.OrthogonalRouter; import org.eclipse.gef.geometry.convert.fx.FX2Geometry; import org.eclipse.gef.geometry.convert.fx.Geometry2FX; import org.eclipse.gef.geometry.euclidean.Vector; import org.eclipse.gef.geometry.planar.Dimension; import org.eclipse.gef.geometry.planar.Point; import org.eclipse.gef.mvc.fx.behaviors.SelectionBehavior; import org.eclipse.gef.mvc.fx.models.HoverModel; import org.eclipse.gef.mvc.fx.parts.AbstractSegmentHandlePart; import org.eclipse.gef.mvc.fx.parts.CircleSegmentHandlePart; import org.eclipse.gef.mvc.fx.parts.IContentPart; import org.eclipse.gef.mvc.fx.parts.IHandlePart; import org.eclipse.gef.mvc.fx.parts.IVisualPart; import org.eclipse.gef.mvc.fx.policies.BendConnectionPolicy; import javafx.geometry.Bounds; import javafx.geometry.Point2D; import javafx.scene.Node; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; /** * The {@link BendFirstAnchorageOnSegmentHandleDragHandler} is an * {@link IOnDragHandler} that can be installed on the handle parts of an * {@link Connection}, so that the user is able to manipulate that connection by * dragging its handles. This policy expects that a handle is created for each * anchor point of the connection (start, way, end), as well as for each middle * point of a segment. Moreover, this policy expects that the respective handles * are of type {@link CircleSegmentHandlePart}. * * @author mwienand * @author anyssen * */ // TODO: this is only applicable to FXSegmentHandlePart hosts public class BendFirstAnchorageOnSegmentHandleDragHandler extends AbstractHandler implements IOnDragHandler { private CursorSupport cursorSupport = new CursorSupport(this); private SnapToGridSupport snapSupport = new SnapToGridSupport(this); private IVisualPart<? extends Connection> targetPart; private boolean isSegmentDragged; private Point initialMouseInScene; private Point handlePositionInScene; private int initialSegmentIndex; private double initialSegmentParameter; private boolean isInvalid = false; private boolean isPrepared; private BendConnectionPolicy bendPolicy; private Point startPositionInScene; private Comparator<IHandlePart<? extends Node>> handleDistanceComparator = new Comparator<IHandlePart<? extends Node>>() { @Override public int compare(IHandlePart<? extends Node> interactedWith, IHandlePart<? extends Node> other) { Bounds otherBounds = other.getVisual().getLayoutBounds(); Point2D otherPosition = other.getVisual().localToScene( otherBounds.getMinX() + otherBounds.getWidth() / 2, otherBounds.getMinY() + otherBounds.getHeight() / 2); // only useful to find the most similar part return (int) (handlePositionInScene .getDistance(FX2Geometry.toPoint(otherPosition)) * 10); } }; @Override public void abortDrag() { if (isInvalid) { return; } restoreRefreshVisuals(targetPart); rollback(bendPolicy); updateHandles(); bendPolicy = null; targetPart = null; } /** * Returns the {@link BendConnectionPolicy} that is installed on the * {@link #getTargetPart()}. * * @return The {@link BendConnectionPolicy} that is installed on the * {@link #getTargetPart()}. */ protected BendConnectionPolicy determineBendPolicy() { // retrieve the default bend policy from the target part return targetPart.getAdapter(BendConnectionPolicy.class); } /** * Determines the target {@link IVisualPart} for this interaction handler. * Per default, the first anchorage of the {@link #getHost()} is returned. * * @return The target {@link IVisualPart} for this interaction handler. */ @SuppressWarnings("unchecked") protected IVisualPart<? extends Connection> determineTargetPart() { return (IVisualPart<? extends Connection>) getHost() .getAnchoragesUnmodifiable().keySet().iterator().next(); } @Override public void drag(MouseEvent e, Dimension delta) { if (isInvalid) { return; } // prepare upon first drag if (!isPrepared) { isPrepared = true; prepareBend(e.isShiftDown(), bendPolicy); // move initially so that the initial positions for the selected // points are computed bendPolicy.move(initialMouseInScene, initialMouseInScene); // query selected position List<Point> initialPositions = bendPolicy .getSelectedInitialPositions(); Point startPositionInConnectionLocal = initialPositions.get(0); startPositionInScene = FX2Geometry.toPoint(targetPart.getVisual() .localToScene(startPositionInConnectionLocal.x, startPositionInConnectionLocal.y)); } // determine constraints Connection connection = targetPart.getVisual(); boolean isOrthogonal = isSegmentDragged && connection.getRouter() instanceof OrthogonalRouter; boolean isHorizontal = isOrthogonal && bendPolicy.isSelectionHorizontal(); // apply mouse-delta to selected-position-in-scene Point endPositionInScene = startPositionInScene.getTranslated(delta); // snap to grid endPositionInScene = isPrecise(e) ? endPositionInScene : snapSupport.snapToGrid(endPositionInScene.x, endPositionInScene.y); // perform changes bendPolicy.move(startPositionInScene, endPositionInScene); // update handles if (isOrthogonal) { if (isHorizontal) { // can only move vertically handlePositionInScene.setY(endPositionInScene.y); } else { // can only move horizontally handlePositionInScene.setX(endPositionInScene.x); } } else { handlePositionInScene.setX(endPositionInScene.x); handlePositionInScene.setY(endPositionInScene.y); } updateHandles(); } @Override public void endDrag(MouseEvent e, Dimension delta) { if (isInvalid) { return; } commit(bendPolicy); restoreRefreshVisuals(targetPart); updateHandles(); bendPolicy = null; targetPart = null; } /** * Returns the {@link BendConnectionPolicy} to use for manipulating the * {@link #getTargetPart()}. * * @return The {@link BendConnectionPolicy} to use for manipulating the * {@link #getTargetPart()}. */ protected BendConnectionPolicy getBendPolicy() { return bendPolicy; } /** * Returns the {@link CursorSupport} of this policy. * * @return The {@link CursorSupport} of this policy. */ protected CursorSupport getCursorSupport() { return cursorSupport; } @Override public AbstractSegmentHandlePart<? extends Node> getHost() { return (AbstractSegmentHandlePart<? extends Node>) super.getHost(); } /** * Returns the target {@link IVisualPart} for this policy that is determined * using {@link #determineTargetPart()} if it is not set, yet. * * @return The target {@link IVisualPart} for this policy. */ protected IVisualPart<? extends Connection> getTargetPart() { return targetPart; } @Override public void hideIndicationCursor() { getCursorSupport().restoreCursor(); } /** * Returns <code>true</code> if the given {@link MouseEvent} should trigger * bend, <code>false</code> otherwise. Otherwise returns <code>false</code> * . By default will always return <code>true</code>. * * @param event * The {@link MouseEvent} in question. * @return <code>true</code> if the given {@link MouseEvent} should trigger * bend, otherwise <code>false</code>. */ protected boolean isBend(MouseEvent event) { return true; } /** * Returns <code>true</code> if precise manipulations should be performed * for the given {@link MouseEvent}. Otherwise returns <code>false</code>. * * @param e * The {@link MouseEvent} that is used to determine if precise * manipulations should be performed (i.e. if the corresponding * modifier key is pressed). * @return <code>true</code> if precise manipulations should be performed, * <code>false</code> otherwise. */ protected boolean isPrecise(MouseEvent e) { return e.isShortcutDown(); } /** * Prepares the given {@link BendConnectionPolicy} for the manipulation of * its host part. * * @param isShiftDown * <code>true</code> if shift is pressed, otherwise * <code>false</code>. * @param bendPolicy * {@link BendConnectionPolicy} of the target part. */ private void prepareBend(boolean isShiftDown, BendConnectionPolicy bendPolicy) { AbstractSegmentHandlePart<? extends Node> host = getHost(); if (host.getSegmentParameter() == 0.5) { if (isShiftDown || targetPart.getVisual() .getRouter() instanceof OrthogonalRouter) { // move segment, copy ends when connected bendPolicy.selectSegment(host.getSegmentIndex()); isSegmentDragged = true; } else { // create new way point in middle and move it (disabled for // orthogonal connections) Integer previousAnchorHandle = bendPolicy .getExplicitIndexAtOrBefore(host.getSegmentIndex()); Integer newAnchorHandle = bendPolicy .createAfter(previousAnchorHandle, initialMouseInScene); // select for manipulation bendPolicy.select(newAnchorHandle); } } else if (host.getSegmentParameter() == 0.25 || host.getSegmentParameter() == 0.75) { // split segment isSegmentDragged = true; boolean selectFirstHalve = host.getSegmentParameter() == 0.25; // determine segment indices for neighbor anchors int firstSegmentIndex = host.getSegmentIndex(); int secondSegmentIndex = host.getSegmentIndex() + 1; // determine middle of segment Point firstPoint = targetPart.getVisual() .getPoint(firstSegmentIndex); Point secondPoint = targetPart.getVisual() .getPoint(secondSegmentIndex); Vector direction = new Vector(firstPoint, secondPoint); Point midPoint = firstPoint.getTranslated(direction.x / 2, direction.y / 2); Point2D midInScene = targetPart.getVisual().localToScene(midPoint.x, midPoint.y); // determine connected status of start or end point (depending on // which side of the segment is moved after splitting) Node connectedAnchorage = targetPart.getVisual().getAnchor( selectFirstHalve ? firstSegmentIndex : secondSegmentIndex) .getAnchorage(); boolean isConnected = connectedAnchorage != null && connectedAnchorage != targetPart.getVisual(); // make the anchors at the segment indices explicit List<Integer> explicit = bendPolicy.makeExplicit(firstSegmentIndex, secondSegmentIndex); Integer firstAnchorHandle = explicit.get(0); Integer secondAnchorHandle = explicit.get(1); // copy start/end if it is connected so that the copy can be // selected for movement if (isConnected) { // compute connection index for point to copy // TODO: Remove duplicate code (see // BendConnectionOperation#getConnectionIndex(int)). int explicitCount = -1; Connection connection = bendPolicy.getHost().getVisual(); int connectionIndex = 0; for (; connectionIndex < connection.getAnchorsUnmodifiable() .size(); connectionIndex++) { IAnchor a = connection.getAnchor(connectionIndex); if (!connection.getRouter().wasInserted(a)) { explicitCount++; } if (explicitCount == (selectFirstHalve ? firstAnchorHandle : secondAnchorHandle)) { // found all operation indices break; } } // determine position in scene for point to copy Point positionInScene = FX2Geometry .toPoint(targetPart.getVisual() .localToScene(Geometry2FX.toFXPoint( bendPolicy.getHost().getVisual() .getPoint(connectionIndex)))); // copy the anchor if (selectFirstHalve) { firstAnchorHandle = bendPolicy .createAfter(firstAnchorHandle, positionInScene); } else { secondAnchorHandle = bendPolicy .createBefore(secondAnchorHandle, positionInScene); } } // create new anchor at segment's middle and copy that new anchor so // that the copy can be selected for movement if (selectFirstHalve) { secondAnchorHandle = bendPolicy.createAfter(firstAnchorHandle, FX2Geometry.toPoint(midInScene)); secondAnchorHandle = bendPolicy.createAfter(firstAnchorHandle, FX2Geometry.toPoint(midInScene)); } else { firstAnchorHandle = bendPolicy.createAfter(firstAnchorHandle, FX2Geometry.toPoint(midInScene)); firstAnchorHandle = bendPolicy.createAfter(firstAnchorHandle, FX2Geometry.toPoint(midInScene)); // increment second anchor handle because we added 2 points // before that secondAnchorHandle += 2; } // select the anchors for movement bendPolicy.select(firstAnchorHandle); bendPolicy.select(secondAnchorHandle); } else { // compute connection index from handle part data int connectionIndex = host.getSegmentIndex() + (host.getSegmentParameter() == 1 ? 1 : 0); // make anchor explicit if it is implicit bendPolicy.select(bendPolicy .makeExplicit(connectionIndex, connectionIndex).get(0)); } } @Override public boolean showIndicationCursor(KeyEvent event) { return false; } @Override public boolean showIndicationCursor(MouseEvent event) { return false; } @Override public void startDrag(MouseEvent e) { isInvalid = !isBend(e); if (isInvalid) { return; } isPrepared = false; isSegmentDragged = false; initialMouseInScene = new Point(e.getSceneX(), e.getSceneY()); handlePositionInScene = initialMouseInScene.getCopy(); AbstractSegmentHandlePart<? extends Node> hostPart = getHost(); initialSegmentIndex = hostPart.getSegmentIndex(); initialSegmentParameter = hostPart.getSegmentParameter(); targetPart = determineTargetPart(); storeAndDisableRefreshVisuals(targetPart); bendPolicy = determineBendPolicy(); init(bendPolicy); updateHandles(); } /** * Re-computes the handle parts. Adjusts the host to reflect its new * position. */ @SuppressWarnings("unchecked") protected void updateHandles() { if (!(targetPart instanceof IContentPart)) { return; } IContentPart<? extends Node> targetContentPart = (IContentPart<? extends Node>) targetPart; IHandlePart<? extends Node> replacementHandle = targetPart.getRoot() .getAdapter(SelectionBehavior.class).updateHandles( targetContentPart, handleDistanceComparator, getHost()); if (replacementHandle instanceof AbstractSegmentHandlePart) { AbstractSegmentHandlePart<Node> segmentData = (AbstractSegmentHandlePart<Node>) replacementHandle; getHost().setSegmentIndex(segmentData.getSegmentIndex()); getHost().setSegmentParameter(segmentData.getSegmentParameter()); if (segmentData.getSegmentParameter() == initialSegmentParameter) { // Restore hover if the replacement handle fulfills the same // role as the host (same parameter == same role). getHost().getRoot().getViewer().getAdapter(HoverModel.class) .setHover(getHost()); } else if (!((initialSegmentParameter == 0.25 || initialSegmentParameter == 0.75) && segmentData.getSegmentParameter() == 0.5 && Math.abs(segmentData.getSegmentIndex() - initialSegmentIndex) < 2)) { // XXX: If a quarter handle was dragged and replaced by a mid // handle, we do not clear hover. getHost().getRoot().getViewer().getAdapter(HoverModel.class) .clearHover(); } } } }