/*******************************************************************************
* 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
*
*******************************************************************************/
package org.eclipse.gef.mvc.examples.logo.handlers;
import java.util.ArrayList;
import java.util.List;
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.AffineTransform;
import org.eclipse.gef.geometry.planar.Point;
import org.eclipse.gef.mvc.fx.handlers.AbstractHandler;
import org.eclipse.gef.mvc.fx.handlers.IOnClickHandler;
import org.eclipse.gef.mvc.fx.operations.ReverseUndoCompositeOperation;
import org.eclipse.gef.mvc.fx.parts.DefaultHoverFeedbackPartFactory;
import org.eclipse.gef.mvc.fx.parts.IContentPart;
import org.eclipse.gef.mvc.fx.parts.IRootPart;
import org.eclipse.gef.mvc.fx.policies.CreationPolicy;
import org.eclipse.gef.mvc.fx.policies.TransformPolicy;
import org.eclipse.gef.mvc.fx.viewer.InfiniteCanvasViewer;
import com.google.common.collect.HashMultimap;
import com.google.common.reflect.TypeToken;
import com.google.inject.Provider;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.event.EventTarget;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.effect.DropShadow;
import javafx.scene.effect.Effect;
import javafx.scene.effect.Reflection;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Affine;
/**
* The {@link CreationMenuOnClickHandler} displays a context menu that can be
* used to create content.
*
* @author wienand
*
*/
// TODO: only applicable for LayeredRootPart and InfiniteCanvasViewer
public class CreationMenuOnClickHandler extends AbstractHandler implements IOnClickHandler {
/**
* An {@link ICreationMenuItem} can be displayed by an
* {@link CreationMenuOnClickHandler}.
*
* @author wienand
*
*/
// TODO: re-use content part to retrieve visual
public static interface ICreationMenuItem {
/**
* Returns a newly created content element that is added to the viewer
* when this menu item is selected.
*
* @return The content element that is created when this menu item is
* selected.
*/
public Object createContent();
/**
* Returns the visual for this menu item.
*
* @return The visual for this menu item.
*/
public Node createVisual();
}
/**
* The adapter role for the
* <code>Provider<List<IFXCreationMenuItem>></code>.
*/
public static final String MENU_ITEM_PROVIDER_ROLE = "Provider<List<ICreationMenuItem>>";
/**
* Default {@link Paint} used for to fill the interior of the arrows.
*/
private static final Paint ARROW_FILL = Color.WHITE;
/**
* Default {@link Paint} used for to stroke the outside of the arrows.
*/
private static final Paint ARROW_STROKE = Color.web("#5a61af");
/**
* Default stroke width for the arrows.
*/
private static final double ARROW_STROKE_WIDTH = 2.5;
/**
* Set of points used for the left (smaller as, <code><</code>) arrow.
*/
private static final Double[] LEFT_ARROW_POINTS = new Double[] { 10d, 0d, 0d, 5d, 10d, 10d };
/**
* Radius of the drop shadow effects.
*/
private static final double DROP_SHADOW_RADIUS = 5;
/**
* Set of points used for the right (greater than, <code>></code>) arrow.
*/
private static final Double[] RIGHT_ARROW_POINTS = new Double[] { 0d, 0d, 10d, 5d, 0d, 10d };
private static Reflection createDropShadowReflectionEffect(double effectRadius, Color color) {
DropShadow dropShadow = new DropShadow(effectRadius, color);
Reflection reflection = new Reflection();
reflection.setInput(dropShadow);
return reflection;
}
private static boolean isNested(Parent parent, Node node) {
while (node != null && node != parent) {
node = node.getParent();
}
return node == parent;
}
/**
* List of {@link ICreationMenuItem}s which can be constructed.
*/
private final List<ICreationMenuItem> items = new ArrayList<>();
/**
* The index of the current item in the list of {@link #geometries}.
*/
private int currentItemIndex = 1;
/**
* The initial mouse position in the coordinate system of the scene of the
* {@link #getHost() host}.
*/
private Point initialMousePositionInScene;
/**
* Stores the padding around visuals used to circumvent translation issues
* when applying a drop shadow effect.
*/
private final double padding = DROP_SHADOW_RADIUS + 1 + ARROW_STROKE_WIDTH * 2 + 1;
/**
* The {@link HBox} in which all menu visuals are layed out.
*/
private HBox hbox;
/**
* The {@link Group} managing the template visual.
*/
private Group templateGroup;
@Override
public void click(MouseEvent e) {
// open menu on right click
if (MouseButton.SECONDARY.equals(e.getButton())) {
// close menu if already open
if (isMenuOpen()) {
closeMenu();
}
EventTarget target = e.getTarget();
if (target instanceof Node) {
initialMousePositionInScene = new Point(e.getSceneX(), e.getSceneY());
// query menu items and reset index
refreshMenuItems();
setCurrentItemIndex(0);
openMenu(e);
}
} else if (MouseButton.PRIMARY.equals(e.getButton())) {
// close menu if currently opened
if (isMenuOpen()) {
EventTarget target = e.getTarget();
if (target instanceof Node) {
Node targetNode = (Node) target;
if (!isNested(hbox, targetNode)) {
closeMenu();
}
}
}
}
}
private void closeMenu() {
// remove menu visuals
getViewer().getCanvas().getScrolledOverlayGroup().getChildren().remove(hbox);
}
private Node createArrow(final boolean left) {
// shape
final Polygon arrow = new Polygon();
arrow.getPoints().addAll(left ? LEFT_ARROW_POINTS : RIGHT_ARROW_POINTS);
// style
arrow.setStrokeWidth(ARROW_STROKE_WIDTH);
arrow.setStroke(ARROW_STROKE);
arrow.setFill(ARROW_FILL);
// effect
effectOnHover(arrow, new DropShadow(DROP_SHADOW_RADIUS, getHighlightColor()));
// action
arrow.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
traverse(left);
}
});
return arrow;
}
private Node createMenuItem() {
// create visual
templateGroup = new Group();
updateTemplateVisual();
// highlighting
templateGroup.setEffect(createDropShadowReflectionEffect(DROP_SHADOW_RADIUS, Color.TRANSPARENT));
effectOnHover(templateGroup, createDropShadowReflectionEffect(DROP_SHADOW_RADIUS, getHighlightColor()));
// register click action
templateGroup.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
onItemClick();
}
});
// register scroll action
templateGroup.setOnScroll(new EventHandler<ScrollEvent>() {
@Override
public void handle(ScrollEvent event) {
traverse(event.getDeltaY() < 0);
}
});
return templateGroup;
}
private void effectOnHover(final Node node, final Effect effect) {
final Effect[] oldEffect = new Effect[1];
node.setOnMouseEntered(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
oldEffect[0] = node.getEffect();
node.setEffect(effect);
}
});
node.setOnMouseExited(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
node.setEffect(oldEffect[0]);
}
});
}
/**
* Returns the index of the currently displayed menu item.
*
* @return The index of the currently displayed menu item.
*/
protected int getCurrentItemIndex() {
return currentItemIndex;
}
/**
* Returns the {@link Color} that is used to stroke hover feedback.
*
* @return The {@link Color} that is used to stroke hover feedback.
*/
@SuppressWarnings("serial")
protected Color getHighlightColor() {
Provider<Color> hoverFeedbackColorProvider = getViewer()
.getAdapter(AdapterKey.get(new TypeToken<Provider<Color>>() {
}, DefaultHoverFeedbackPartFactory.HOVER_FEEDBACK_COLOR_PROVIDER));
return hoverFeedbackColorProvider == null ? DefaultHoverFeedbackPartFactory.DEFAULT_HOVER_FEEDBACK_COLOR
: hoverFeedbackColorProvider.get();
}
/**
* Returns the list containing the {@link ICreationMenuItem}s that are
* displayed by this policy.
*
* @return the list containing the {@link ICreationMenuItem}s that are
* displayed by this policy.
*/
protected List<ICreationMenuItem> getItems() {
return items;
}
/**
* Returns the {@link InfiniteCanvasViewer} in which to open the creation
* menu.
*
* @return The {@link InfiniteCanvasViewer} in which to open the creation
* menu.
*/
protected InfiniteCanvasViewer getViewer() {
return (InfiniteCanvasViewer) getHost().getRoot().getViewer();
}
/**
* Returns <code>true</code> if the creation menu is currently open.
* Otherwise returns <code>false</code>.
*
* @return <code>true</code> if the creation menu is currently open,
* <code>false</code> otherwise.
*/
protected boolean isMenuOpen() {
return hbox != null && hbox.getParent() != null;
}
/**
* Callback method called when an item is clicked.
*/
protected void onItemClick() {
// compute width and height deltas to the content layer
Node itemVisual = templateGroup.getChildren().get(0);
Bounds bounds = itemVisual.getLayoutBounds();
Bounds boundsInContent = getHost().getRoot().getVisual().sceneToLocal(itemVisual.localToScene(bounds));
double dx = bounds.getWidth() - boundsInContent.getWidth();
double dy = bounds.getHeight() - boundsInContent.getHeight();
// compute translation based on the bounds, scaling, and width/height
// deltas
Affine contentsTransform = getViewer().getCanvas().contentTransformProperty().get();
double x = boundsInContent.getMinX() - bounds.getMinX() / contentsTransform.getMxx() - dx / 2;
double y = boundsInContent.getMinY() - bounds.getMinY() / contentsTransform.getMyy() - dy / 2;
// close the creation menu
closeMenu();
// create the new semantic element
ICreationMenuItem item = items.get(currentItemIndex);
Object toCreate = item.createContent();
// build create operation
IRootPart<? extends Node> root = getHost().getRoot();
CreationPolicy creationPolicy = root.getAdapter(CreationPolicy.class);
creationPolicy.init();
IContentPart<? extends Node> contentPart = creationPolicy.create(toCreate, root,
root.getContentPartChildren().size(), HashMultimap.<IContentPart<? extends Node>, String> create(),
false, false);
// relocate to final position
TransformPolicy txPolicy = contentPart.getAdapter(TransformPolicy.class);
txPolicy.init();
txPolicy.setTransform(new AffineTransform(1, 0, 0, 1, x, y));
// assemble operations
ReverseUndoCompositeOperation rev = new ReverseUndoCompositeOperation("CreateOnClick");
rev.add(creationPolicy.commit());
rev.add(txPolicy.commit());
try {
getViewer().getDomain().execute(rev, new NullProgressMonitor());
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
/**
* Opens the creation menu.
*
* @param e
* The {@link MouseEvent} that activated the creation menu.
*/
protected void openMenu(final MouseEvent e) {
// compute max width and height
double maxWidth = 0;
double maxHeight = 0;
for (ICreationMenuItem item : items) {
Bounds bounds = item.createVisual().getLayoutBounds();
if (bounds.getWidth() > maxWidth) {
maxWidth = bounds.getWidth();
}
if (bounds.getHeight() > maxHeight) {
maxHeight = bounds.getHeight();
}
}
// construct content pane and group
Node leftArrow = createArrow(true);
Node menuItem = createMenuItem();
Node rightArrow = createArrow(false);
hbox = new HBox();
hbox.getChildren().addAll(wrapWithPadding(leftArrow, padding),
wrapWithPadding(menuItem, padding, maxWidth, maxHeight), wrapWithPadding(rightArrow, padding));
// place into overlay group
final Group overlayGroup = getViewer().getCanvas().getScrolledOverlayGroup();
overlayGroup.getChildren().add(hbox);
hbox.layoutBoundsProperty().addListener(new ChangeListener<Bounds>() {
@Override
public void changed(ObservableValue<? extends Bounds> observable, Bounds oldBounds, Bounds newBounds) {
Affine contentTransform = getViewer().getCanvas().getContentTransform();
hbox.setTranslateX(-newBounds.getWidth() / 2);
hbox.setTranslateY(-newBounds.getHeight() / 2);
hbox.setScaleX(contentTransform.getMxx());
hbox.setScaleY(contentTransform.getMyy());
Point2D pos = overlayGroup.sceneToLocal(initialMousePositionInScene.x, initialMousePositionInScene.y);
hbox.setLayoutX(pos.getX());
hbox.setLayoutY(pos.getY());
}
});
}
/**
* Refreshes the menu. Queries the menu items using the provider that is
* registered under the {@link #MENU_ITEM_PROVIDER_ROLE}.
*/
protected void refreshMenuItems() {
@SuppressWarnings("serial")
List<ICreationMenuItem> menuItems = getHost().<Provider<List<ICreationMenuItem>>> getAdapter(
AdapterKey.get(new TypeToken<Provider<List<ICreationMenuItem>>>() {
}, MENU_ITEM_PROVIDER_ROLE)).get();
List<ICreationMenuItem> items = getItems();
items.clear();
items.addAll(menuItems);
}
/**
* Changes the displayed menu item to the item at the given index.
*
* @param currentItemIndex
* The index of the menu item to show.
*/
protected void setCurrentItemIndex(int currentItemIndex) {
this.currentItemIndex = currentItemIndex;
}
/**
* Traverses the menu items.
*
* @param previous
* <code>true</code> to show the previous item,
* <code>false</code> to show the next item.
*/
protected void traverse(final boolean previous) {
if (previous) {
// show previous geometry
setCurrentItemIndex(getCurrentItemIndex() - 1);
if (getCurrentItemIndex() < 0) {
setCurrentItemIndex(items.size() - 1);
}
} else {
// show next geometry
setCurrentItemIndex(getCurrentItemIndex() + 1);
if (getCurrentItemIndex() >= items.size()) {
setCurrentItemIndex(0);
}
}
updateTemplateVisual();
}
/**
* Refreshes the visualization of the item at index
* {@link #getCurrentItemIndex()}.
*/
protected void updateTemplateVisual() {
// exchange template visual
templateGroup.getChildren().clear();
templateGroup.getChildren().add(getItems().get(getCurrentItemIndex()).createVisual());
}
private StackPane wrapWithPadding(Node node, double padding) {
return wrapWithPadding(node, padding, node.getLayoutBounds().getWidth(), node.getLayoutBounds().getHeight());
}
private StackPane wrapWithPadding(Node node, double padding, double width, double height) {
StackPane stack = new StackPane();
stack.getChildren()
.addAll(new Rectangle(width + padding + padding, height + padding + padding, Color.TRANSPARENT), node);
return stack;
}
}