/*******************************************************************************
* 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
*
*******************************************************************************/
package org.eclipse.gef.mvc.fx.handlers;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import org.eclipse.gef.geometry.convert.fx.FX2Geometry;
import org.eclipse.gef.geometry.convert.fx.Geometry2FX;
import org.eclipse.gef.geometry.euclidean.Angle;
import org.eclipse.gef.geometry.planar.AffineTransform;
import org.eclipse.gef.geometry.planar.Dimension;
import org.eclipse.gef.geometry.planar.IGeometry;
import org.eclipse.gef.geometry.planar.Point;
import org.eclipse.gef.geometry.planar.Rectangle;
import org.eclipse.gef.mvc.fx.models.SelectionModel;
import org.eclipse.gef.mvc.fx.parts.AbstractSegmentHandlePart;
import org.eclipse.gef.mvc.fx.parts.IContentPart;
import org.eclipse.gef.mvc.fx.policies.ResizePolicy;
import org.eclipse.gef.mvc.fx.policies.TransformPolicy;
import org.eclipse.gef.mvc.fx.providers.ResizableTransformableBoundsProvider;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
/**
* The {@link ResizeTransformSelectedOnHandleDragHandler} is an
* {@link IOnDragHandler} that relocates and scales the whole
* {@link SelectionModel selection} when its host (a box selection handle,
* {@link AbstractSegmentHandlePart}) is dragged.
*
* @author mwienand
*
*/
public class ResizeTransformSelectedOnHandleDragHandler
extends AbstractHandler implements IOnDragHandler {
private CursorSupport cursorSupport = new CursorSupport(this);
private SnapToGridSupport snapSupport = new SnapToGridSupport(this);
private Point initialMouseLocation = null;
private Rectangle selectionBounds;
private Map<IContentPart<? extends Node>, Double> relX1 = null;
private Map<IContentPart<? extends Node>, Double> relY1 = null;
private Map<IContentPart<? extends Node>, Double> relX2 = null;
private Map<IContentPart<? extends Node>, Double> relY2 = null;
private boolean invalidGesture = false;
private Map<IContentPart<? extends Node>, Integer> scaleIndices = new HashMap<>();
private Map<IContentPart<? extends Node>, Integer> translateIndices = new HashMap<>();
private List<IContentPart<? extends Node>> targetParts;
/**
* Default constructor.
*/
public ResizeTransformSelectedOnHandleDragHandler() {
}
@Override
public void abortDrag() {
if (invalidGesture) {
return;
}
// rollback transactional policies
for (IContentPart<? extends Node> part : targetParts) {
TransformPolicy transformPolicy = getTransformPolicy(part);
if (transformPolicy != null) {
restoreRefreshVisuals(part);
rollback(transformPolicy);
ResizePolicy resizePolicy = getResizePolicy(part);
if (resizePolicy != null) {
rollback(resizePolicy);
}
}
}
// clear transformation indices lists
scaleIndices.clear();
translateIndices.clear();
// null resize context vars
selectionBounds = null;
initialMouseLocation = null;
relX1 = relY1 = relX2 = relY2 = null;
}
/**
* Computes the relative x and y coordinates for the given target part and
* stores them in the {@link #relX1}, {@link #relY1}, {@link #relX2}, and
* {@link #relY2} maps.
*
* @param targetPart
*/
private void computeRelatives(IContentPart<? extends Node> targetPart) {
Rectangle bounds = getVisualBounds(targetPart);
if (bounds != null) {
double left = bounds.getX() - selectionBounds.getX();
double right = left + bounds.getWidth();
double top = bounds.getY() - selectionBounds.getY();
double bottom = top + bounds.getHeight();
double selWidth = selectionBounds.getWidth();
double selHeight = selectionBounds.getHeight();
relX1.put(targetPart, left / selWidth);
relX2.put(targetPart, right / selWidth);
relY1.put(targetPart, top / selHeight);
relY2.put(targetPart, bottom / selHeight);
}
}
@Override
public void drag(MouseEvent e, Dimension delta) {
if (invalidGesture) {
return;
}
if (selectionBounds == null) {
return;
}
if (targetParts.isEmpty()) {
return;
}
// snap to grid
// FIXME: apply resize-transform first, then snap the moved vertex to
// the next grid position and update the values
Point newEndPointInScene = isPrecise(e)
? new Point(e.getSceneX(), e.getSceneY())
: snapSupport.snapToGrid(e.getSceneX(), e.getSceneY());
// update selection bounds
Rectangle sel = updateSelectionBounds(newEndPointInScene);
// update target parts
for (IContentPart<? extends Node> targetPart : targetParts) {
// compute initial and new bounds for this target
Bounds initialBounds = getBounds(selectionBounds, targetPart);
Bounds newBounds = getBounds(sel, targetPart);
// System.out.println(targetPart.getClass().getSimpleName()
// + " bounds change from " + initialBounds.getMinX() + ", "
// + initialBounds.getMinY() + " : " + initialBounds.getWidth()
// + " x " + initialBounds.getHeight() + " to "
// + newBounds.getMinX() + ", " + newBounds.getMinY() + " : "
// + newBounds.getWidth() + " x " + newBounds.getHeight()
// + ".");
// compute translation in scene coordinates
double dx = newBounds.getMinX() - initialBounds.getMinX();
double dy = newBounds.getMinY() - initialBounds.getMinY();
// transform translation to parent coordinates
Node visual = targetPart.getVisual();
Point2D originInParent = visual.getParent().sceneToLocal(0, 0);
Point2D deltaInParent = visual.getParent().sceneToLocal(dx, dy);
dx = deltaInParent.getX() - originInParent.getX();
dy = deltaInParent.getY() - originInParent.getY();
// apply translation
getTransformPolicy(targetPart)
.setPostTranslate(translateIndices.get(targetPart), dx, dy);
// check if we can resize the part
AffineTransform affineTransform = getTransformPolicy(targetPart)
.getCurrentTransform();
if (affineTransform.getRotation().equals(Angle.fromDeg(0))) {
// no rotation => resize possible
// TODO: special case 90 degree rotations
double dw = newBounds.getWidth() - initialBounds.getWidth();
double dh = newBounds.getHeight() - initialBounds.getHeight();
// System.out.println(
// "delta size in scene: " + dw + ", " + dh + ".");
Point2D originInLocal = visual.sceneToLocal(newBounds.getMinX(),
newBounds.getMinY());
Point2D dstInLocal = visual.sceneToLocal(
newBounds.getMinX() + dw, newBounds.getMinY() + dh);
dw = dstInLocal.getX() - originInLocal.getX();
dh = dstInLocal.getY() - originInLocal.getY();
// System.out.println(
// "delta size in local: " + dw + ", " + dh + ".");
getResizePolicy(targetPart).resize(dw, dh);
} else {
// compute scaling based on bounds change
double sx = newBounds.getWidth() / initialBounds.getWidth();
double sy = newBounds.getHeight() / initialBounds.getHeight();
// apply scaling
getTransformPolicy(targetPart)
.setPostScale(scaleIndices.get(targetPart), sx, sy);
}
}
}
@Override
public void endDrag(MouseEvent e, Dimension delta) {
if (invalidGesture) {
invalidGesture = false;
return;
}
for (IContentPart<? extends Node> part : targetParts) {
TransformPolicy transformPolicy = getTransformPolicy(part);
if (transformPolicy != null) {
restoreRefreshVisuals(part);
commit(transformPolicy);
ResizePolicy resizePolicy = getResizePolicy(part);
if (resizePolicy != null) {
commit(resizePolicy);
}
}
}
// clear transformation indices lists
scaleIndices.clear();
translateIndices.clear();
// null resize context vars
selectionBounds = null;
initialMouseLocation = null;
relX1 = relY1 = relX2 = relY2 = null;
}
private Bounds getBounds(Rectangle sel,
IContentPart<? extends Node> targetPart) {
double x1 = sel.getX() + sel.getWidth() * relX1.get(targetPart);
double x2 = sel.getX() + sel.getWidth() * relX2.get(targetPart);
double y1 = sel.getY() + sel.getHeight() * relY1.get(targetPart);
double y2 = sel.getY() + sel.getHeight() * relY2.get(targetPart);
return new BoundingBox(x1, y1, x2 - x1, y2 - y1);
}
/**
* Returns the {@link CursorSupport} of this policy.
*
* @return The {@link CursorSupport} of this policy.
*/
protected CursorSupport getCursorSupport() {
return cursorSupport;
}
@SuppressWarnings("unchecked")
@Override
public AbstractSegmentHandlePart<Node> getHost() {
return (AbstractSegmentHandlePart<Node>) super.getHost();
}
/**
* Returns the {@link ResizePolicy} that is installed on the given
* {@link IContentPart}.
*
* @param part
* The {@link IContentPart} of which the {@link ResizePolicy} is
* returned.
* @return The {@link ResizePolicy} that is installed on the given
* {@link IContentPart}.
*/
protected ResizePolicy getResizePolicy(IContentPart<? extends Node> part) {
return part.getAdapter(ResizePolicy.class);
}
/**
* Returns the unioned {@link #getVisualBounds(IContentPart) bounds} of all
* target parts.
*
* @param targetParts
* @return the unioned visual bounds of all target parts
*/
private Rectangle getSelectionBounds(
List<IContentPart<? extends Node>> targetParts) {
if (targetParts.isEmpty()) {
throw new IllegalArgumentException("No target parts given.");
}
Rectangle bounds = getVisualBounds(targetParts.get(0));
if (targetParts.size() == 1) {
return bounds;
}
ListIterator<IContentPart<? extends Node>> iterator = targetParts
.listIterator(1);
while (iterator.hasNext()) {
IContentPart<? extends Node> cp = iterator.next();
if (bounds == null) {
bounds = getVisualBounds(cp);
} else {
bounds.union(getVisualBounds(cp));
}
}
return bounds;
}
/**
* Returns a {@link List} containing all {@link IContentPart}s that should
* be scaled/relocated by this policy. Per default, the whole
* {@link SelectionModel selection} is returned.
*
* @return A {@link List} containing all {@link IContentPart}s that should
* be scaled/relocated by this policy.
*/
protected List<IContentPart<? extends Node>> getTargetParts() {
List<IContentPart<? extends Node>> selection = new ArrayList<>(
getHost().getRoot().getViewer().getAdapter(SelectionModel.class)
.getSelectionUnmodifiable());
// filter out parts without transformable-resizable bounds
Iterator<IContentPart<? extends Node>> it = selection.iterator();
while (it.hasNext()) {
if (getVisualBounds(it.next()) == null) {
it.remove();
}
}
return selection;
}
/**
* Returns the {@link TransformPolicy} that is installed on the given
* {@link IContentPart}.
*
* @param part
* The {@link IContentPart} of which the {@link TransformPolicy}
* is returned.
* @return The {@link TransformPolicy} that is installed on the given
* {@link IContentPart}.
*/
protected TransformPolicy getTransformPolicy(
IContentPart<? extends Node> part) {
return part.getAdapter(TransformPolicy.class);
}
/**
* Returns a {@link Rectangle} representing the visual bounds of the given
* {@link IContentPart} within the coordinate system of the {@link Scene}.
*
* @param contentPart
* The {@link IContentPart} of which the visual bounds are
* computed.
* @return A {@link Rectangle} representing the visual bounds of the given
* {@link IContentPart} within the coordinate system of the
* {@link Scene}.
*/
protected Rectangle getVisualBounds(
IContentPart<? extends Node> contentPart) {
if (contentPart == null) {
throw new IllegalArgumentException("contentPart may not be null!");
}
// use provider to compute bounds
ResizableTransformableBoundsProvider boundsProvider = new ResizableTransformableBoundsProvider();
boundsProvider.setAdaptable(contentPart);
IGeometry boundsInLocal = boundsProvider.get();
// transform to scene
return boundsInLocal == null ? null
: FX2Geometry.toRectangle(contentPart.getVisual().localToScene(
Geometry2FX.toFXBounds(boundsInLocal.getBounds())));
}
@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 <code>true</code> if the given {@link MouseEvent} should trigger
* resize and transform of the selected parts. Otherwise returns
* <code>false</code>. Per default, returns <code>true</code> if
* <code><Control></code> is not pressed and at least two target parts
* are present.
*
* @param event
* The {@link ScrollEvent} in question.
* @return <code>true</code> to indicate that the given {@link ScrollEvent}
* should trigger panning, otherwise <code>false</code>.
*/
protected boolean isResizeTransform(MouseEvent event) {
return targetParts.size() > 0 && !event.isControlDown()
&& getHost().getRoot().getViewer()
.getAdapter(SelectionModel.class)
.getSelectionUnmodifiable().size() > 1;
}
@Override
public boolean showIndicationCursor(KeyEvent event) {
return false;
}
@Override
public boolean showIndicationCursor(MouseEvent event) {
return false;
}
@Override
public void startDrag(MouseEvent e) {
targetParts = getTargetParts();
invalidGesture = !isResizeTransform(e);
if (invalidGesture) {
return;
}
// init resize context vars
initialMouseLocation = new Point(e.getSceneX(), e.getSceneY());
selectionBounds = getSelectionBounds(targetParts);
relX1 = new HashMap<>();
relY1 = new HashMap<>();
relX2 = new HashMap<>();
relY2 = new HashMap<>();
// init scale relocate policies
for (IContentPart<? extends Node> targetPart : targetParts) {
TransformPolicy transformPolicy = getTransformPolicy(targetPart);
if (transformPolicy != null) {
storeAndDisableRefreshVisuals(targetPart);
computeRelatives(targetPart);
init(transformPolicy);
// transform scale pivot to parent coordinates
Point pivotInScene = getVisualBounds(targetPart).getTopLeft();
Point pivotInParent = FX2Geometry
.toPoint(getHost().getVisual().getParent()
.sceneToLocal(pivotInScene.x, pivotInScene.y));
// create transformations for scaling around pivot
int translateToOriginIndex = transformPolicy
.createPostTransform();
int scaleIndex = transformPolicy.createPostTransform();
int translateBackIndex = transformPolicy.createPostTransform();
// set translation transforms for scaling
transformPolicy.setPostTranslate(translateToOriginIndex,
-pivotInParent.x, -pivotInParent.y);
transformPolicy.setPostTranslate(translateBackIndex,
pivotInParent.x, pivotInParent.y);
// save rotation index for later adjustments
scaleIndices.put(targetPart, scaleIndex);
// create transform for translation of the target part
translateIndices.put(targetPart,
transformPolicy.createPostTransform());
// initialize resize policy if available
ResizePolicy resizePolicy = getResizePolicy(targetPart);
if (resizePolicy != null) {
init(resizePolicy);
}
}
}
}
/**
* Returns updated selection bounds. The initial selection bounds are copied
* and the copy is shrinked or expanded depending on the mouse location
* change and the handle edge (top, bottom, left, or right).
*
* @param mouseLocation
* @return
*/
private Rectangle updateSelectionBounds(Point endPointInScene) {
Rectangle sel = selectionBounds.getCopy();
double dx = endPointInScene.x - initialMouseLocation.x;
double dy = endPointInScene.y - initialMouseLocation.y;
int segment = getHost().getSegmentIndex();
if (segment == 0 || segment == 3) {
sel.shrink(dx, 0, 0, 0);
} else if (segment == 1 || segment == 2) {
sel.expand(0, 0, dx, 0);
}
if (segment == 0 || segment == 1) {
sel.shrink(0, dy, 0, 0);
} else if (segment == 2 || segment == 3) {
sel.expand(0, 0, 0, dy);
}
return sel;
}
}