/******************************************************************************* * 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 * *******************************************************************************/ package org.eclipse.gef.mvc.fx.handlers; import java.util.ArrayList; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import org.eclipse.gef.fx.nodes.Connection; import org.eclipse.gef.fx.nodes.OrthogonalRouter; import org.eclipse.gef.fx.utils.NodeUtils; import org.eclipse.gef.geometry.convert.fx.FX2Geometry; import org.eclipse.gef.geometry.convert.fx.Geometry2FX; import org.eclipse.gef.geometry.planar.Dimension; import org.eclipse.gef.geometry.planar.Point; import org.eclipse.gef.geometry.planar.Rectangle; import org.eclipse.gef.mvc.fx.models.GridModel; import org.eclipse.gef.mvc.fx.models.SelectionModel; import org.eclipse.gef.mvc.fx.parts.IContentPart; import org.eclipse.gef.mvc.fx.policies.TransformPolicy; import org.eclipse.gef.mvc.fx.viewer.IViewer; import javafx.scene.Node; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.util.Pair; /** * The {@link TranslateSelectedOnDragHandler} is an {@link IOnDragHandler} that * relocates its {@link #getHost() host} when it is dragged with the mouse. * * @author anyssen * */ public class TranslateSelectedOnDragHandler extends AbstractHandler implements IOnDragHandler { private CursorSupport cursorSupport = new CursorSupport(this); private SnapToGridSupport snapToGridSupport = new SnapToGridSupport(this); private SnapToGeometrySupport snapToGeometrySupport; private Point initialMouseLocationInScene = null; private Map<IContentPart<? extends Node>, Integer> translationIndices = new HashMap<>(); private List<Pair<IContentPart<? extends Node>, TransformPolicy>> targets; // gesture validity private boolean invalidGesture = false; private Map<IContentPart<? extends Node>, Rectangle> boundsInScene = new IdentityHashMap<>(); private Rectangle initialLayoutBoundsInScene; @Override public void abortDrag() { if (invalidGesture) { return; } // roll back changes for all target parts for (Pair<IContentPart<? extends Node>, TransformPolicy> pair : targets) { rollback(pair.getValue()); restoreRefreshVisuals(pair.getKey()); } // clear snapping locations if (snapToGeometrySupport != null) { snapToGeometrySupport.stopSnapping(); snapToGeometrySupport = null; } // reset targets targets = null; // reset initial pointer location setInitialMouseLocationInScene(null); // reset translation indices translationIndices.clear(); } @Override public void drag(MouseEvent e, Dimension delta) { // abort this policy if no target parts could be found if (invalidGesture) { return; } // determine viewer IViewer viewer = getHost().getRoot().getViewer(); // prepare data for snap-to-grid Node gridLocalVisual = null; GridModel gridModel = null; double granularityX = 0d; double granularityY = 0d; boolean performSnapping = !isPrecise(e); if (performSnapping) { granularityX = snapToGridSupport.getSnapToGridGranularityX(); granularityY = snapToGridSupport.getSnapToGridGranularityY(); gridModel = viewer.getAdapter(GridModel.class); gridLocalVisual = snapToGridSupport.getGridLocalVisual(viewer); } // apply changes to the target parts for (Pair<IContentPart<? extends Node>, TransformPolicy> pair : targets) { // determine start and end position in scene coordinates Point startInScene = boundsInScene.get(pair.getKey()).getTopLeft(); Point endInScene = startInScene.getTranslated(delta); // snap to location if (performSnapping && snapToGeometrySupport != null) { Dimension snapDimension = snapToGeometrySupport.snapToLocation( Geometry2FX.toFXBounds(initialLayoutBoundsInScene .getTranslated(delta.width, delta.height))); endInScene.translate(snapDimension); } // snap to grid Point newEndInScene = endInScene.getCopy(); if (gridLocalVisual != null) { newEndInScene = snapToGridSupport.snapToGrid(endInScene.x, endInScene.y, gridModel, granularityX, granularityY, gridLocalVisual); } // compute delta in parent coordinates Point newEndInParent = NodeUtils.sceneToLocal( pair.getKey().getVisual().getParent(), newEndInScene); Point startInParent = NodeUtils.sceneToLocal( pair.getKey().getVisual().getParent(), startInScene); Point deltaInParent = newEndInParent .getTranslated(startInParent.getNegated()); // update transformation pair.getValue().setPostTranslate( translationIndices.get(pair.getKey()), deltaInParent.x, deltaInParent.y); } } @Override public void endDrag(MouseEvent e, Dimension delta) { // abort this policy if no target parts could be found if (invalidGesture) { return; } // commit changes for all target parts for (Pair<IContentPart<? extends Node>, TransformPolicy> pair : targets) { commit(pair.getValue()); restoreRefreshVisuals(pair.getKey()); } // clear snapping locations if (snapToGeometrySupport != null) { snapToGeometrySupport.stopSnapping(); snapToGeometrySupport = null; } // reset target parts targets = null; // reset initial pointer location setInitialMouseLocationInScene(null); // reset translation indices translationIndices.clear(); } /** * Returns the {@link CursorSupport} of this policy. * * @return The {@link CursorSupport} of this policy. */ protected CursorSupport getCursorSupport() { return cursorSupport; } /** * Returns the initial mouse location in scene coordinates. * * @return The initial mouse location in scene coordinates. */ protected Point getInitialMouseLocationInScene() { return initialMouseLocationInScene; } /** * Returns a {@link List} containing all {@link IContentPart}s that should * be relocated by this policy. * * @return A {@link List} containing all {@link IContentPart}s that should * be relocated by this policy. */ protected List<IContentPart<? extends Node>> getTargetParts() { return getHost().getRoot().getViewer().getAdapter(SelectionModel.class) .getSelectionUnmodifiable(); } /** * Returns the {@link TransformPolicy} that is installed on the given * {@link IContentPart}. * * @param part * The {@link IContentPart} for which to return the installed * {@link TransformPolicy}. * @return The {@link TransformPolicy} that is installed on the given * {@link IContentPart}. */ protected TransformPolicy getTransformPolicy( IContentPart<? extends Node> part) { return part.getAdapter(TransformPolicy.class); } @Override public void hideIndicationCursor() { getCursorSupport().restoreCursor(); } /** * 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(); } /** * Returns whether the given {@link MouseEvent} should trigger translation. * Per default, will return <code>true</code> if we have more than one * target part or the single target part does not have a connection with an * orthogonal router. * * @param event * The {@link MouseEvent} in question. * @param targetParts * The list of (provisional) target {@link IContentPart}s. * @return <code>true</code> if the given {@link MouseEvent} should trigger * translation, otherwise <code>false</code>. */ // TODO (bug #493418): This condition needs improvement protected boolean isTranslate(MouseEvent event, List<IContentPart<? extends Node>> targetParts) { // do not translate the only selected part if a // BendOnSegmentDragPolicy is registered for that part and the part is // an orthogonal connection that is connected at source and/or target if (targetParts.size() == 1 && targetParts.get(0) .getAdapter(BendOnSegmentDragHandler.class) != null) { IContentPart<? extends Node> part = targetParts.get(0); Node visual = part.getVisual(); if (visual instanceof Connection && ((Connection) visual) .getRouter() instanceof OrthogonalRouter && (((Connection) visual).isStartConnected() || ((Connection) visual).isEndConnected())) { targetParts = null; } } if (targetParts == null || targetParts.isEmpty()) { // abort this policy if no target parts could be found return false; } return true; } /** * Sets the initial mouse location to the given value. * * @param point * The initial mouse location. */ protected void setInitialMouseLocationInScene(Point point) { initialMouseLocationInScene = point; } @Override public boolean showIndicationCursor(KeyEvent event) { return false; } @Override public boolean showIndicationCursor(MouseEvent event) { return false; } @Override public void startDrag(MouseEvent e) { // determine target parts List<IContentPart<? extends Node>> targetParts = getTargetParts(); targets = new ArrayList<>(); // decide whether to perform translation invalidGesture = !isTranslate(e, targetParts); if (invalidGesture) { return; } // save initial pointer location setInitialMouseLocationInScene(new Point(e.getSceneX(), e.getSceneY())); // initialize this policy for all determined target parts for (IContentPart<? extends Node> part : targetParts) { TransformPolicy policy = part.getAdapter(TransformPolicy.class); if (policy == null) { continue; } targets.add(new Pair<>(part, policy)); // init transaction policy storeAndDisableRefreshVisuals(part); init(policy); translationIndices.put(part, policy.createPostTransform()); // determine shape bounds Rectangle shapeBounds = NodeUtils .getShapeBounds(getHost().getVisual()); Rectangle shapeBoundsInScene = NodeUtils .localToScene(getHost().getVisual(), shapeBounds) .getBounds(); boundsInScene.put(part, shapeBoundsInScene); } // snapping for single selection if (getHost().getRoot().getViewer().getAdapter(SelectionModel.class) .getSelectionUnmodifiable().size() == 1 && getHost().getRoot().getViewer() .getAdapter(SelectionModel.class) .getSelectionUnmodifiable().contains(getHost())) { snapToGeometrySupport = getHost().getRoot() .getAdapter(SnapToGeometrySupport.class); if (snapToGeometrySupport != null) { snapToGeometrySupport.startSnapping( (IContentPart<? extends Node>) getHost()); initialLayoutBoundsInScene = FX2Geometry .toRectangle(getHost().getVisual().localToScene( getHost().getVisual().getLayoutBounds())); } } else { snapToGeometrySupport = null; } } }