/******************************************************************************* * Copyright (c) 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 * *******************************************************************************/ package org.eclipse.gef.mvc.fx.handlers; 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.planar.Dimension; import org.eclipse.gef.geometry.planar.Line; import org.eclipse.gef.geometry.planar.Point; import org.eclipse.gef.geometry.planar.Polyline; import org.eclipse.gef.mvc.fx.behaviors.SelectionBehavior; import org.eclipse.gef.mvc.fx.models.SelectionModel; import org.eclipse.gef.mvc.fx.parts.IContentPart; import org.eclipse.gef.mvc.fx.parts.IVisualPart; import org.eclipse.gef.mvc.fx.policies.BendConnectionPolicy; import javafx.collections.ObservableList; import javafx.scene.Node; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; /** * Uses the {@link BendConnectionPolicy} of its host to move the dragged * connection segment. */ public class BendOnSegmentDragHandler extends AbstractHandler implements IOnDragHandler { private CursorSupport cursorSupport = new CursorSupport(this); private SnapToGridSupport snapSupport = new SnapToGridSupport(this); private Point initialMouseInScene; private boolean isInvalid = false; private boolean isPrepared; private BendConnectionPolicy bendPolicy; @Override public void abortDrag() { if (isInvalid) { return; } rollback(bendPolicy); restoreRefreshVisuals(getHost()); updateHandles(); bendPolicy = null; } /** * Returns the {@link BendConnectionPolicy} of the host. * * @return The {@link BendConnectionPolicy} of the host. */ protected BendConnectionPolicy determineBendPolicy() { return getHost().getAdapter(BendConnectionPolicy.class); } @Override public void drag(MouseEvent e, Dimension delta) { if (isInvalid) { return; } // prepare for manipulation upon first drag if (!isPrepared) { isPrepared = true; prepareBend(bendPolicy); // move initially so that the initial positions for the selected // points are computed bendPolicy.move(initialMouseInScene, initialMouseInScene); // TODO: investigate why the following seems unnecessary: // 1. query selected position // 2. transform selected position to scene } // TODO: investigate why the following seems unnecessary: // 3. apply mouse-delta to selected-position-in-scene // 4. snap selected-position-in-scene unless precise // 5. call move(initial-position-in-scene, snapped-position-in-scene) // snap to grid Point newEndPointInScene = isPrecise(e) ? new Point(e.getSceneX(), e.getSceneY()) : snapSupport.snapToGrid(e.getSceneX(), e.getSceneY()); // perform changes bendPolicy.move(initialMouseInScene, newEndPointInScene); updateHandles(); } @Override public void endDrag(MouseEvent e, Dimension delta) { if (isInvalid) { return; } commit(bendPolicy); restoreRefreshVisuals(getHost()); updateHandles(); bendPolicy = null; } /** * Returns the {@link BendConnectionPolicy} of the host. * * @return The {@link BendConnectionPolicy} of the host. */ protected BendConnectionPolicy getBendPolicy() { return bendPolicy; } /** * Returns the {@link CursorSupport} of this policy. * * @return The {@link CursorSupport} of this policy. */ protected CursorSupport getCursorSupport() { return cursorSupport; } @SuppressWarnings("unchecked") @Override public IVisualPart<Connection> getHost() { return (IVisualPart<Connection>) super.getHost(); } @Override public void hideIndicationCursor() { cursorSupport.restoreCursor(); } /** * Returns <code>true</code> if the given {@link MouseEvent} should trigger * bending. Otherwise returns <code>false</code>. Per default returns * <code>true</code> if a single mouse click is performed. * * @param event * The {@link MouseEvent} in question. * @return <code>true</code> if the given {@link MouseEvent} should trigger * focus and select, otherwise <code>false</code>. */ protected boolean isBend(MouseEvent event) { boolean isInvalid = false; if (!(getHost().getVisual().getRouter() instanceof OrthogonalRouter)) { // abort if non-orthogonal isInvalid = true; } else { IVisualPart<? extends Node> host = getHost(); ObservableList<IContentPart<? extends Node>> selection = host .getRoot().getViewer().getAdapter(SelectionModel.class) .getSelectionUnmodifiable(); if (selection.size() > 1 && selection.contains(host)) { // abort if part of multiple selection isInvalid = true; } else if (!getHost().getVisual().isStartConnected() && !getHost().getVisual().isEndConnected()) { // abort if unconnected isInvalid = true; } } return !isInvalid; } /** * 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. * * @param bendPolicy * The {@link BendConnectionPolicy} that is prepared. */ private void prepareBend(BendConnectionPolicy bendPolicy) { // determine curve in scene coordinates Connection connection = bendPolicy.getHost().getVisual(); // construct polyline for connection points Polyline polyline = new Polyline( connection.getPointsUnmodifiable().toArray(new Point[] {})); Polyline polylineInScene = (Polyline) NodeUtils.localToScene(connection, polyline); // determine pressed segment (nearest to mouse) Line[] segmentsInScene = polylineInScene.getCurves(); double minDistance = -1; int segmentIndex = -1; for (int i = 0; i < segmentsInScene.length; i++) { Line segment = segmentsInScene[i]; Point projection = segment.getProjection(initialMouseInScene); double distance = projection.getDistance(initialMouseInScene); if (minDistance < 0 || distance < minDistance) { minDistance = distance; segmentIndex = i; } } if (segmentIndex < 0) { // it is better to die than to return in failure throw new IllegalStateException("Cannot identify pressed segment."); } // select segment bendPolicy.selectSegment(segmentIndex); } @Override public boolean showIndicationCursor(KeyEvent event) { return false; } @Override public boolean showIndicationCursor(MouseEvent event) { // TODO: Show <|> or ^-v indication cursor for segment movement. // cursorSupport.storeAndReplaceCursor(verticalSegment ? // LEFT_RIGHT_CURSOR : TOP_DOWN_CURSOR); return false; } @Override public void startDrag(MouseEvent e) { isInvalid = !isBend(e); if (isInvalid) { return; } isPrepared = false; // save initial mouse position in scene coordinates initialMouseInScene = new Point(e.getSceneX(), e.getSceneY()); // disable refresh visuals for the host storeAndDisableRefreshVisuals(getHost()); bendPolicy = determineBendPolicy(); init(bendPolicy); updateHandles(); } /** * Updates the selection handles. */ protected void updateHandles() { getHost().getRoot().getAdapter(SelectionBehavior.class) .updateHandles(getHost(), null, null); } }