/*******************************************************************************
* 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 & implementation
*
*******************************************************************************/
package org.eclipse.gef.zest.fx.parts;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.gef.fx.anchors.IAnchor;
import org.eclipse.gef.fx.nodes.Connection;
import org.eclipse.gef.fx.nodes.IConnectionInterpolator;
import org.eclipse.gef.fx.nodes.IConnectionRouter;
import org.eclipse.gef.geometry.planar.Point;
import org.eclipse.gef.graph.Edge;
import org.eclipse.gef.mvc.fx.parts.AbstractContentPart;
import org.eclipse.gef.mvc.fx.parts.IBendableContentPart;
import org.eclipse.gef.mvc.fx.parts.IVisualPart;
import org.eclipse.gef.mvc.fx.providers.IAnchorProvider;
import org.eclipse.gef.zest.fx.ZestProperties;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import javafx.collections.MapChangeListener;
import javafx.scene.Node;
/**
* The {@link EdgePart} is the controller for an {@link Edge} content object. It
* uses {@link Connection} for the visualization.
*
* @author mwienand
*
*/
public class EdgePart extends AbstractContentPart<Connection> implements IBendableContentPart<Connection> {
/**
* The role used for attaching to the source node.
*/
protected static final String SOURCE_ROLE = "SOURCE";
/**
* The role used for attaching to the target node.
*/
protected static final String TARGET_ROLE = "TARGET";
/**
* The CSS class that is assigned to the visual of this {@link EdgePart}.
*/
public static final String CSS_CLASS = "edge";
/**
* The CSS class that is assigned to the {@link Connection#getCurve() curve
* node} of the {@link Connection} of this {@link EdgePart}.
*/
public static final String CSS_CLASS_CURVE = "curve";
/**
* CSS class assigned to the decorations.
*/
public static final String CSS_CLASS_DECORATION = "decoration";
private MapChangeListener<String, Object> edgeAttributesObserver = new MapChangeListener<String, Object>() {
@Override
public void onChanged(MapChangeListener.Change<? extends String, ? extends Object> change) {
if (ZestProperties.ROUTER__E.equals(change.getKey())) {
// if the router changed, re-attach the visual (so we attach to
// a different anchor)
for (Entry<IVisualPart<? extends Node>, String> anchoragesByRole : getAnchoragesUnmodifiable()
.entries()) {
doDetachFromAnchorageVisual(anchoragesByRole.getKey(), anchoragesByRole.getValue());
doAttachToAnchorageVisual(anchoragesByRole.getKey(), anchoragesByRole.getValue());
}
}
refreshVisual();
}
};
@Override
protected void doActivate() {
super.doActivate();
getContent().attributesProperty().addListener(edgeAttributesObserver);
}
@Override
protected void doAddChildVisual(IVisualPart<? extends Node> child, int index) {
if (!getVisual().getChildren().contains(child.getVisual())) {
getVisual().getChildren().add(child.getVisual());
}
}
@Override
protected void doAttachToAnchorageVisual(IVisualPart<? extends Node> anchorage, String role) {
IAnchor anchor = anchorage.getAdapter(IAnchorProvider.class).get(this, role);
if (role.equals(SOURCE_ROLE)) {
getVisual().setStartAnchor(anchor);
} else if (role.equals(TARGET_ROLE)) {
getVisual().setEndAnchor(anchor);
} else {
throw new IllegalArgumentException("Cannot attach to anchor with role <" + role + ">.");
}
}
@Override
protected void doAttachToContentAnchorage(Object contentAnchorage, String role) {
if (SOURCE_ROLE.equals(role)) {
getContent().setSource((org.eclipse.gef.graph.Node) contentAnchorage);
} else if (TARGET_ROLE.equals(role)) {
getContent().setTarget((org.eclipse.gef.graph.Node) contentAnchorage);
} else {
throw new IllegalArgumentException("Cannot attach to content anchorage with role <" + role + ">.");
}
}
@Override
protected Connection doCreateVisual() {
Connection visual = new Connection();
visual.getStyleClass().add(CSS_CLASS);
// initialize style class for (default) curve
visual.getCurve().getStyleClass().add(CSS_CLASS_CURVE);
// initialized style class for (default) decorations
if (visual.getStartDecoration() != null) {
if (!visual.getStartDecoration().getStyleClass().contains(CSS_CLASS_DECORATION)) {
visual.getStartDecoration().getStyleClass().add(CSS_CLASS_DECORATION);
}
}
if (visual.getEndDecoration() != null) {
if (!visual.getEndDecoration().getStyleClass().contains(CSS_CLASS_DECORATION)) {
visual.getEndDecoration().getStyleClass().add(CSS_CLASS_DECORATION);
}
}
return visual;
}
@Override
protected void doDeactivate() {
getContent().attributesProperty().removeListener(edgeAttributesObserver);
super.doDeactivate();
}
@Override
protected void doDetachFromAnchorageVisual(IVisualPart<? extends Node> anchorage, String role) {
Connection connection = getVisual();
if (role.equals(SOURCE_ROLE)) {
Point startPoint = connection.getStartPoint();
connection.setStartPoint(startPoint == null ? new Point() : startPoint);
} else if (role.equals(TARGET_ROLE)) {
Point endPoint = connection.getEndPoint();
connection.setEndPoint(endPoint == null ? new Point() : endPoint);
} else {
throw new IllegalArgumentException("Cannot detach from anchor with role <" + role + ">.");
}
}
@Override
protected void doDetachFromContentAnchorage(Object contentAnchorage, String role) {
if (SOURCE_ROLE.equals(role)) {
getContent().setSource(null);
} else if (TARGET_ROLE.equals(role)) {
getContent().setTarget(null);
} else {
throw new IllegalArgumentException("Cannot detach from content anchorage with role <" + role + ">.");
}
}
@Override
protected SetMultimap<? extends Object, String> doGetContentAnchorages() {
SetMultimap<Object, String> anchorages = HashMultimap.create();
org.eclipse.gef.graph.Node source = getContent().getSource();
if (source != null) {
anchorages.put(source, SOURCE_ROLE);
}
org.eclipse.gef.graph.Node target = getContent().getTarget();
if (target != null) {
anchorages.put(target, TARGET_ROLE);
}
return anchorages;
}
@Override
protected List<? extends Object> doGetContentChildren() {
return Collections.emptyList();
}
@Override
protected void doRefreshVisual(Connection visual) {
Edge edge = getContent();
Map<String, Object> attrs = edge.attributesProperty();
refreshCurve();
// css class
if (attrs.containsKey(ZestProperties.CSS_CLASS__NE)) {
String cssClass = ZestProperties.getCssClass(edge);
if (!visual.getStyleClass().contains(cssClass)) {
visual.getStyleClass().add(cssClass);
}
}
// css id
if (attrs.containsKey(ZestProperties.CSS_ID__NE)) {
String cssId = ZestProperties.getCssId(edge);
visual.setId(cssId);
}
// css style
String curveCssStyle = ZestProperties.getCurveCssStyle(edge);
if (attrs.containsKey(ZestProperties.CURVE_CSS_STYLE__E)) {
visual.getCurve().setStyle(curveCssStyle);
}
// custom decoration
Node sourceDecoration = ZestProperties.getSourceDecoration(edge);
if (sourceDecoration != null) {
visual.setStartDecoration(sourceDecoration);
// apply CSS class
if (!sourceDecoration.getStyleClass().contains(CSS_CLASS_DECORATION)) {
sourceDecoration.getStyleClass().add(CSS_CLASS_DECORATION);
}
}
// apply source decoration CSS style (even if decoration is not set via
// property)
String sourceDecorationCssStyle = ZestProperties.getSourceDecorationCssStyle(edge);
if (sourceDecorationCssStyle != null && visual.getStartDecoration() != null) {
visual.getStartDecoration().setStyle(sourceDecorationCssStyle);
}
Node targetDecoration = ZestProperties.getTargetDecoration(edge);
if (targetDecoration != null) {
visual.setEndDecoration(targetDecoration);
// apply CSS class
if (!targetDecoration.getStyleClass().contains(CSS_CLASS_DECORATION)) {
targetDecoration.getStyleClass().add(CSS_CLASS_DECORATION);
}
}
// apply target decoration CSS style (even if decoration is not set via
// property)
String targetDecorationCssStyle = ZestProperties.getTargetDecorationCssStyle(edge);
if (targetDecorationCssStyle != null && visual.getEndDecoration() != null) {
visual.getEndDecoration().setStyle(targetDecorationCssStyle);
}
// connection router
IConnectionRouter router = ZestProperties.getRouter(edge);
if (router != null) {
visual.setRouter(router);
}
// interpolator
IConnectionInterpolator interpolator = ZestProperties.getInterpolator(edge);
if (interpolator != null) {
visual.setInterpolator(interpolator);
}
// TODO: replace the following code with
// setVisualBendPoints(getContentBendPoints());
// start point or hint
Point startPoint = ZestProperties.getStartPoint(edge);
if (!getContentAnchoragesUnmodifiable().containsValue(SOURCE_ROLE)) {
if (startPoint != null) {
visual.setStartPoint(startPoint);
}
} else {
visual.setStartPointHint(startPoint);
}
// end point or hint
Point endPoint = ZestProperties.getEndPoint(edge);
if (!getContentAnchoragesUnmodifiable().containsValue(TARGET_ROLE)) {
if (endPoint != null) {
visual.setEndPoint(endPoint);
}
} else {
visual.setEndPointHint(endPoint);
}
// control points
List<Point> controlPoints = new ArrayList<>(ZestProperties.getControlPoints(edge));
if (!visual.getControlPoints().equals(controlPoints)) {
visual.setControlPoints(controlPoints);
}
}
@Override
protected void doRemoveChildVisual(IVisualPart<? extends Node> child, int index) {
getVisual().getChildren().remove(child.getVisual());
}
@Override
public Edge getContent() {
return (Edge) super.getContent();
}
@Override
public List<BendPoint> getContentBendPoints() {
List<BendPoint> bendPoints = new ArrayList<>();
// determine start point, end point, and control points from content
Edge edge = getContent();
Point startPoint = ZestProperties.getStartPoint(edge);
Point endPoint = ZestProperties.getEndPoint(edge);
List<Point> controlPoints = ZestProperties.getControlPoints(edge);
if (startPoint == null || endPoint == null) {
return bendPoints;
}
// add start bend point
if (edge.getSource() == null) {
bendPoints.add(new BendPoint(startPoint));
} else {
bendPoints.add(new BendPoint(edge.getSource(), startPoint));
}
// add control bend points
for (Point cp : controlPoints) {
bendPoints.add(new BendPoint(cp));
}
// add end bend point
if (edge.getTarget() == null) {
bendPoints.add(new BendPoint(endPoint));
} else {
bendPoints.add(new BendPoint(edge.getTarget(), endPoint));
}
return bendPoints;
}
/**
* Returns the {@link Node} that displays the edge.
*
* @return The {@link Node} used to display the edge.
*/
public Node getCurve() {
return getVisual().getCurve();
}
private void refreshCurve() {
Node curve = ZestProperties.getCurve(getContent());
if (getVisual().getCurve() != curve && curve != null) {
getVisual().setCurve(curve);
if (!curve.getStyleClass().contains(CSS_CLASS_CURVE)) {
curve.getStyleClass().add(CSS_CLASS_CURVE);
}
}
}
@Override
public void setContentBendPoints(List<org.eclipse.gef.mvc.fx.parts.IBendableContentPart.BendPoint> bendPoints) {
// disable refreshing of visuals
boolean wasRefreshVisual = isRefreshVisual();
setRefreshVisual(false);
// collect positions and de-/attach source & target
List<Point> positions = new ArrayList<>();
boolean attachedSource = false;
boolean attachedTarget = false;
for (int i = 0; i < bendPoints.size(); i++) {
BendPoint bp = bendPoints.get(i);
if (i == 0) {
// update source
org.eclipse.gef.graph.Node newSource = bp.isAttached()
? (org.eclipse.gef.graph.Node) bp.getContentAnchorage() : null;
org.eclipse.gef.graph.Node oldSource = getContent().getSource();
if (oldSource != newSource) {
if (oldSource != null) {
detachFromContentAnchorage(oldSource, SOURCE_ROLE);
}
if (newSource != null) {
attachToContentAnchorage(newSource, SOURCE_ROLE);
attachedSource = true;
}
} else if (oldSource != null) {
attachedSource = true;
}
}
if (i == bendPoints.size() - 1) {
// update target
org.eclipse.gef.graph.Node newTarget = bp.isAttached()
? (org.eclipse.gef.graph.Node) bp.getContentAnchorage() : null;
org.eclipse.gef.graph.Node oldTarget = getContent().getTarget();
if (oldTarget != newTarget) {
if (oldTarget != null) {
detachFromContentAnchorage(oldTarget, TARGET_ROLE);
}
if (newTarget != null) {
attachToContentAnchorage(newTarget, TARGET_ROLE);
attachedTarget = true;
}
} else if (oldTarget != null) {
attachedTarget = true;
}
}
if (!bp.isAttached()) {
positions.add(bp.getPosition());
}
}
// update properties
if (!attachedSource) {
if (positions.size() > 0) {
ZestProperties.setStartPoint(getContent(), positions.remove(0));
} else {
throw new IllegalStateException("No start point provided.");
}
} else {
// XXX: Set start hint as Zest start point property so it can be
// used within doRefreshVisual().
ZestProperties.setStartPoint(getContent(), getVisual().getStartPointHint());
}
if (!attachedTarget) {
if (positions.size() > 0) {
ZestProperties.setEndPoint(getContent(), positions.remove(positions.size() - 1));
} else {
throw new IllegalStateException("No end point provided.");
}
} else {
// XXX: Set start hint as Zest start point property so it can be
// used within doRefreshVisual().
ZestProperties.setEndPoint(getContent(), getVisual().getEndPointHint());
}
ZestProperties.setControlPoints(getContent(), positions);
// restore refreshing of visuals
setRefreshVisual(wasRefreshVisual);
refreshVisual();
}
}