/*******************************************************************************
* Copyright (c) 2015, 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) - contribution for Bugzilla #451852
*
*******************************************************************************/
package org.eclipse.gef.mvc.fx.handlers;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.gef.common.adapt.AdapterKey;
import org.eclipse.gef.geometry.planar.Dimension;
import org.eclipse.gef.mvc.fx.operations.SelectOperation;
import org.eclipse.gef.mvc.fx.parts.AbstractFeedbackPart;
import org.eclipse.gef.mvc.fx.parts.DefaultSelectionFeedbackPartFactory;
import org.eclipse.gef.mvc.fx.parts.IContentPart;
import org.eclipse.gef.mvc.fx.parts.IFeedbackPart;
import org.eclipse.gef.mvc.fx.parts.IRootPart;
import org.eclipse.gef.mvc.fx.parts.IVisualPart;
import org.eclipse.gef.mvc.fx.parts.PartUtils;
import org.eclipse.gef.mvc.fx.viewer.IViewer;
import com.google.common.reflect.TypeToken;
import com.google.inject.Provider;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
/**
* The {@link MarqueeOnDragHandler} is an {@link IOnDragHandler} that performs
* marquee selection when the mouse is dragged. The start and end position of
* the mouse span a marquee area. Everything within that area will be selected.
*
* @author anyssen
* @author mwienand
*
*/
public class MarqueeOnDragHandler extends AbstractHandler
implements IOnDragHandler {
private static double[] bbox(Point2D start, Point2D end) {
double bbox[] = { start.getX(), start.getY(), end.getX(), end.getY() };
double tmp;
if (bbox[0] > bbox[2]) {
tmp = bbox[0];
bbox[0] = bbox[2];
bbox[2] = tmp;
}
if (bbox[1] > bbox[3]) {
tmp = bbox[1];
bbox[1] = bbox[3];
bbox[3] = tmp;
}
return bbox;
}
/**
* Returns a {@link List} of all {@link Node}s that are descendants of the
* given root {@link Node} and fully contained within the bounds specified
* by <code>[x0, y0, x1, y1]</code>.
*
* @param root
* The root {@link Node}.
* @param x0
* The minimum x-coordinate.
* @param y0
* The minimum y-coordinate.
* @param x1
* The maximum x-coordinate.
* @param y1
* The maximum y-coordinate.
* @return A {@link List} containing all {@link Node}s that are descendants
* of the given root {@link Node} and fully contained within the
* specified bounds.
*/
// TODO: move to utility
public static List<Node> findContainedNodes(Node root, double x0, double y0,
double x1, double y1) {
Bounds bounds;
double bx1, bx0, by1, by0;
List<Node> containedNodes = new ArrayList<>();
Queue<Node> nodes = new LinkedList<>();
nodes.add(root);
while (!nodes.isEmpty()) {
Node current = nodes.remove();
bounds = current.getBoundsInLocal();
bounds = current.localToScene(bounds);
bx1 = bounds.getMaxX();
bx0 = bounds.getMinX();
by1 = bounds.getMaxY();
by0 = bounds.getMinY();
if (bx1 < x0 || bx0 > x1 || by1 < y0 || by0 > y1) {
// current node is outside of marquee bounds => dont collect
} else {
if (bx0 >= x0 && bx1 <= x1 && by0 >= y0 && by1 <= y1) {
// current node is fully contained within marquee bounds
containedNodes.add(current);
}
if (current instanceof Parent) {
// add all children to nodes
Parent p = (Parent) current;
nodes.addAll(p.getChildrenUnmodifiable());
}
}
}
return containedNodes;
}
private CursorSupport cursorSupport = new CursorSupport(this);
// stores upon press() if the press-drag-release gesture is invalid
private boolean invalidGesture = false;
// mouse coordinates
private Point2D startPosInRoot;
private Point2D endPosInRoot;
// feedback
private IFeedbackPart<? extends Node> feedback;
@Override
public void abortDrag() {
if (!invalidGesture && feedback != null) {
removeFeedback();
}
}
/**
* Adds a feedback rectangle to the root part of the {@link #getHost() host}
* . The rectangle will show the marquee area.
*/
protected void addFeedback() {
if (feedback != null) {
removeFeedback();
}
feedback = new AbstractFeedbackPart<Rectangle>() {
@Override
protected void doActivate() {
super.doActivate();
setRefreshVisual(true);
}
@Override
protected Rectangle doCreateVisual() {
Rectangle visual = new Rectangle();
visual.setFill(Color.TRANSPARENT);
visual.setStroke(getPrimarySelectionColor());
visual.setStrokeWidth(1);
visual.setStrokeType(StrokeType.CENTERED);
visual.getStrokeDashArray().setAll(5d, 5d);
return visual;
}
@Override
protected void doRefreshVisual(Rectangle visual) {
IRootPart<? extends Node> root = getRoot();
Point2D start = visual.sceneToLocal(
root.getVisual().localToScene(startPosInRoot));
Point2D end = visual.sceneToLocal(
root.getVisual().localToScene(endPosInRoot));
double[] bbox = bbox(start, end);
// offset x and y by half a pixel to ensure the rectangle gets a
// hairline stroke
visual.setX(bbox[0] - 0.5);
visual.setY(bbox[1] - 0.5);
visual.setWidth(bbox[2] - bbox[0]);
visual.setHeight(bbox[3] - bbox[1]);
}
};
getHost().getRoot().addChild(feedback);
}
@Override
public void drag(MouseEvent e, Dimension delta) {
if (invalidGesture) {
return;
}
endPosInRoot = getHost().getRoot().getVisual()
.sceneToLocal(e.getSceneX(), e.getSceneY());
updateFeedback();
}
@Override
public void endDrag(MouseEvent e, Dimension delta) {
if (invalidGesture) {
return;
}
// compute bounding box in scene coordinates
IRootPart<? extends Node> root = getHost().getRoot();
Node rootVisual = root.getVisual();
endPosInRoot = rootVisual.sceneToLocal(e.getSceneX(), e.getSceneY());
Point2D start = rootVisual.localToScene(startPosInRoot);
Point2D end = rootVisual.localToScene(endPosInRoot);
double[] bbox = bbox(start, end);
// find nodes contained in bbox
List<Node> nodes = findContainedNodes(rootVisual.getScene().getRoot(),
bbox[0], bbox[1], bbox[2], bbox[3]);
// find content parts for contained nodes
List<IContentPart<? extends Node>> parts = getParts(nodes);
// filter out all parts that are not selectable
Iterator<IContentPart<? extends Node>> it = parts.iterator();
while (it.hasNext()) {
if (!it.next().isSelectable()) {
it.remove();
}
}
// select the selectable parts contained within the marquee area
try {
root.getViewer().getDomain().execute(
new SelectOperation(root.getViewer(), parts),
new NullProgressMonitor());
} catch (ExecutionException e1) {
throw new IllegalStateException(e1);
}
removeFeedback();
}
/**
* Returns the {@link CursorSupport} of this policy.
*
* @return The {@link CursorSupport} of this policy.
*/
protected CursorSupport getCursorSupport() {
return cursorSupport;
}
/**
* Returns a {@link List} containing all {@link IContentPart}s that are
* corresponding to the given {@link List} of {@link Node}s.
*
* @param nodes
* The {@link List} of {@link Node}s for which the corresponding
* {@link IContentPart}s are returned.
* @return A {@link List} containing all {@link IContentPart}s that are
* corresponding to the given {@link Node}s.
*/
protected List<IContentPart<? extends Node>> getParts(List<Node> nodes) {
List<IContentPart<? extends Node>> parts = new ArrayList<>();
IViewer viewer = getHost().getRoot().getViewer();
for (Node node : nodes) {
IVisualPart<? extends Node> part = PartUtils
.retrieveVisualPart(viewer, node);
if (part != null && part instanceof IContentPart
&& !parts.contains(part)) {
parts.add((IContentPart<? extends Node>) part);
}
}
return parts;
}
/**
* Returns the primary selection {@link Color}.
*
* @return The primary selection {@link Color}.
*/
protected Color getPrimarySelectionColor() {
@SuppressWarnings("serial")
Provider<Color> connectedColorProvider = getHost().getRoot().getViewer()
.getAdapter(AdapterKey.get(new TypeToken<Provider<Color>>() {
}, DefaultSelectionFeedbackPartFactory.PRIMARY_SELECTION_FEEDBACK_COLOR_PROVIDER));
return connectedColorProvider == null
? DefaultSelectionFeedbackPartFactory.DEFAULT_PRIMARY_SELECTION_FEEDBACK_COLOR
: connectedColorProvider.get();
}
@Override
public void hideIndicationCursor() {
getCursorSupport().restoreCursor();
}
/**
* Returns <code>true</code> if the given {@link MouseEvent} should trigger
* marquee selection. Otherwise returns <code>false</code>. Per default
* returns <code>true</code> if the event target is not registered.
*
* @param event
* The {@link MouseEvent} in question.
* @return <code>true</code> if the given {@link KeyEvent} should trigger
* zooming, otherwise <code>false</code>.
*/
protected boolean isMarquee(MouseEvent event) {
return !isRegistered(event.getTarget());
}
/**
* Removes the feedback rectangle.
*/
protected void removeFeedback() {
if (feedback != null) {
getHost().getRoot().removeChild(feedback);
feedback = null;
}
}
@Override
public boolean showIndicationCursor(KeyEvent event) {
return false;
}
@Override
public boolean showIndicationCursor(MouseEvent event) {
return false;
}
@Override
public void startDrag(MouseEvent e) {
invalidGesture = !isMarquee(e);
if (invalidGesture) {
return;
}
startPosInRoot = getHost().getRoot().getVisual()
.sceneToLocal(e.getSceneX(), e.getSceneY());
endPosInRoot = new Point2D(startPosInRoot.getX(),
startPosInRoot.getY());
addFeedback();
}
/**
* Updates the feedback rectangle.
*/
protected void updateFeedback() {
if (feedback != null) {
feedback.refreshVisual();
}
}
}