blob: f915ee12e14aeedf783b1c28451c0b0a5dee350e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014, 2020 THALES GLOBAL SERVICES and others.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
* IBM Corporation - The code from org.eclipse.gmf.runtime.draw2d.ui.internal.routers.RectilinearRouter
*******************************************************************************/
package org.eclipse.sirius.diagram.ui.internal.operation;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import org.eclipse.draw2d.Connection;
import org.eclipse.draw2d.ConnectionLayer;
import org.eclipse.draw2d.ConnectionRouter;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.PointList;
import org.eclipse.draw2d.geometry.PrecisionPoint;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.gef.EditPart;
import org.eclipse.gef.GraphicalEditPart;
import org.eclipse.gef.LayerConstants;
import org.eclipse.gef.editparts.LayerManager;
import org.eclipse.gmf.runtime.common.ui.services.editor.EditorService;
import org.eclipse.gmf.runtime.diagram.ui.editparts.ConnectionEditPart;
import org.eclipse.gmf.runtime.diagram.ui.parts.DiagramEditor;
import org.eclipse.gmf.runtime.draw2d.ui.figures.BaseSlidableAnchor;
import org.eclipse.gmf.runtime.draw2d.ui.internal.figures.ConnectionLayerEx;
import org.eclipse.gmf.runtime.draw2d.ui.internal.routers.RectilinearRouter;
import org.eclipse.gmf.runtime.notation.Anchor;
import org.eclipse.gmf.runtime.notation.Bendpoints;
import org.eclipse.gmf.runtime.notation.Edge;
import org.eclipse.gmf.runtime.notation.IdentityAnchor;
import org.eclipse.gmf.runtime.notation.Node;
import org.eclipse.gmf.runtime.notation.NotationFactory;
import org.eclipse.gmf.runtime.notation.RelativeBendpoints;
import org.eclipse.gmf.runtime.notation.Routing;
import org.eclipse.gmf.runtime.notation.View;
import org.eclipse.sirius.common.ui.tools.api.util.EclipseUIUtil;
import org.eclipse.sirius.diagram.DEdge;
import org.eclipse.sirius.diagram.EdgeStyle;
import org.eclipse.sirius.diagram.description.CenteringStyle;
import org.eclipse.sirius.diagram.ui.business.api.query.ConnectionQuery;
import org.eclipse.sirius.diagram.ui.business.api.query.EdgeQuery;
import org.eclipse.sirius.diagram.ui.business.internal.operation.AbstractModelChangeOperation;
import org.eclipse.sirius.diagram.ui.graphical.figures.CanonicalRectilinearRouter;
import org.eclipse.sirius.diagram.ui.internal.edit.parts.DEdgeEditPart;
import org.eclipse.sirius.diagram.ui.internal.refresh.GMFHelper;
import org.eclipse.sirius.diagram.ui.tools.internal.routers.RectilinearEdgeUtil;
import org.eclipse.sirius.diagram.ui.tools.internal.util.GMFNotationUtilities;
import org.eclipse.sirius.ext.base.Option;
import org.eclipse.sirius.ext.base.Options;
import org.eclipse.sirius.ext.gmf.runtime.editparts.GraphicalHelper;
import org.eclipse.ui.IEditorPart;
/**
* A Model Change Operation to center the edge source and/or target. The
* operation will compute the edge anchor and bendpoints according to the
* {@link EdgeStyle#isSourceCentered()} and {@link EdgeStyle#isTargetCentered()}
* values.
*
* @author Florian Barbin
*
*/
@SuppressWarnings("restriction")
public class CenterEdgeEndModelChangeOperation extends AbstractModelChangeOperation<Void> {
private Edge edge;
private PrecisionPoint existingSourceAnchorAbsoluteLocation;
private PrecisionPoint existingTargetAnchorAbsoluteLocation;
private PrecisionPoint newTargetAnchorAbsoluteLocation;
private PrecisionPoint newSourceAnchorAbsoluteLocation;
private ConnectionEditPart connectionEditPart;
private Connection connection;
private boolean useFigure = true;
private Dimension sourceFigureSize;
private Dimension targetFigureSize;
private static final String CENTERED_ANCHOR = GMFNotationUtilities.getTerminalString(0.5d, 0.5d);
/**
* Constructor to use the connectionEditPart to compute the new edge
* bendpoints.
*
* @param connectionEditPart
* the edge connectionEditPart.
* @param edge
* the GMF edge.
*/
public CenterEdgeEndModelChangeOperation(ConnectionEditPart connectionEditPart, Edge edge) {
this(edge);
this.connectionEditPart = connectionEditPart;
this.connection = (Connection) connectionEditPart.getFigure();
}
/**
* Default constructor to center the given gmf edge ends (if needed). This
* operation will try to retrieve the corresponding figure (if possible) to
* compute the new bendpoints.
*
* @param edge
* the GMF edge to center ends.
*/
public CenterEdgeEndModelChangeOperation(Edge edge) {
this.edge = edge;
}
/**
* Constructor to explicitly specify whether this operation should try to
* retrieve the edge figure to compute the new bendpoints.
*
* @param edge
* the GMF edge.
* @param useFigure
* true if this operation should use the corresponding draw2D
* figure, false otherwise.
*/
public CenterEdgeEndModelChangeOperation(Edge edge, boolean useFigure) {
this(edge);
this.useFigure = useFigure;
}
/**
* Set the source and target figure size. Those size will be used instead of
* compute them.
*
* @param sourceFigureSize
* the edge source figure size. If null, the size will be
* computed.
* @param targetFigureSize
* the edge target figure size. If null, the size will be
* computed.
*/
public void setSourceAndTargetSize(Dimension sourceFigureSize, Dimension targetFigureSize) {
this.sourceFigureSize = sourceFigureSize;
this.targetFigureSize = targetFigureSize;
}
@Override
public Void execute() {
Routing routingValue = getRoutingValue();
// we do not handle Tree routing style
if (!Routing.TREE_LITERAL.equals(routingValue)) {
if (isEdgeSourceCentered() && isEdgeTargetCentered()) {
centerEdgeEnds(CenteringStyle.BOTH, routingValue);
} else if (isEdgeSourceCentered()) {
centerEdgeEnds(CenteringStyle.SOURCE, routingValue);
}
else if (isEdgeTargetCentered()) {
centerEdgeEnds(CenteringStyle.TARGET, routingValue);
}
}
return null;
}
private Routing getRoutingValue() {
Routing routingValue = Routing.MANUAL_LITERAL;
EdgeQuery edgeQuery = new EdgeQuery(edge);
if (edgeQuery.isEdgeWithTreeRoutingStyle()) {
routingValue = Routing.TREE_LITERAL;
} else if (edgeQuery.isEdgeWithRectilinearRoutingStyle()) {
routingValue = Routing.RECTILINEAR_LITERAL;
}
return routingValue;
}
/**
* Center the connection end (Source, Target or Both) according to the
* center value.
*
* @param center
* the {@link CenteringStyle} value.
*/
private void centerEdgeEnds(CenteringStyle center, Routing routingValue) {
Bendpoints bendpoints = edge.getBendpoints();
if (bendpoints instanceof RelativeBendpoints) {
View edgeSourceView = edge.getSource();
View edgeTargetView = edge.getTarget();
if (edgeSourceView instanceof Node && edgeTargetView instanceof Node) {
setConnectionIfNull();
// We get the edge source and target nodes absolute bounds to
// compute absolute anchors coordinates
Option<Rectangle> sourceBounds = getAbsoluteSourceBounds(edgeSourceView);
Option<Rectangle> targetBounds = getAbsoluteTargetBounds(edgeTargetView);
if (sourceBounds.some() && targetBounds.some()) {
// Calculate the existing anchors absolute location
retrieveAndSetExistingAnchorsAbsoluteLocation(sourceBounds.get(), targetBounds.get());
// Compute the new anchors absolute coordinates according to
// the center value.
computeAndSetNewAnchorsAbsoluteCoordinates(sourceBounds.get(), targetBounds.get(), center);
PointList newPointList = new PointList();
// Compute the existing bendpoints absolute location
newPointList = new ConnectionQuery(connection).getAbsolutePointList((RelativeBendpoints) bendpoints, existingSourceAnchorAbsoluteLocation, existingTargetAnchorAbsoluteLocation);
if (!(Routing.RECTILINEAR_LITERAL.getLiteral().equals(routingValue.getLiteral()))) {
// The default case of straight edge:
handleStraightCase(center, sourceBounds.get(), targetBounds.get(), newPointList);
} else {
// the rectilinear case
handleRectilinearCase(center, sourceBounds.get(), targetBounds.get(), newPointList);
}
GMFNotationUtilities.setGMFBendpoints(edge, newPointList, newSourceAnchorAbsoluteLocation, newTargetAnchorAbsoluteLocation);
}
}
}
}
/**
* Because of the difference between the GMF absolute bounds computation and
* the draw2D absolute bounds, we will compute these bounds from draw2D if
* we have access to the figure, or from GMF if we work in "pure" GMF
* context.
*
* @param gmfView
* the GMFView to compute the absolute bounds.
* @param isSource
* true if it represents the edge source, false otherwise.
* @return the absolute bounds.
*/
private Option<Rectangle> getAbsoluteBounds(View gmfView, boolean isSource) {
if (connectionEditPart != null) {
EditPart editPart = null;
if (isSource) {
editPart = connectionEditPart.getSource();
} else {
editPart = connectionEditPart.getTarget();
}
if (editPart instanceof GraphicalEditPart) {
return Options.newSome(GraphicalHelper.getAbsoluteBoundsIn100Percent((GraphicalEditPart) editPart));
}
}
return GMFHelper.getAbsoluteBounds(gmfView, true, true);
}
private Option<Rectangle> getAbsoluteSourceBounds(View edgeSourceView) {
Option<Rectangle> option = getAbsoluteBounds(edgeSourceView, true);
if (sourceFigureSize != null && option.some()) {
Rectangle rectangle = option.get();
rectangle.setSize(sourceFigureSize);
}
return option;
}
private Option<Rectangle> getAbsoluteTargetBounds(View edgeTargetView) {
Option<Rectangle> option = getAbsoluteBounds(edgeTargetView, false);
if (targetFigureSize != null && option.some()) {
Rectangle rectangle = option.get();
rectangle.setSize(targetFigureSize);
}
return option;
}
/**
* Deal with the straight edge case.
*
* @param center
* the {@link Center} value: {@link Center#SOURCE}
* {@link Center#TARGET} or {@link Center#BOTH}.
* @param sourceBounds
* the source figure bounds.
* @param targetBounds
* the target figure bounds.
* @param existingPointList
* the existing point list before centering any connection end.
*/
private void handleStraightCase(CenteringStyle center, Rectangle sourceBounds, Rectangle targetBounds, PointList existingPointList) {
Point sourceLineOrigin = newTargetAnchorAbsoluteLocation;
Point targetLineOrigin = newSourceAnchorAbsoluteLocation;
Point sourceLineTerminus = newSourceAnchorAbsoluteLocation;
Point targetLineTerminus = newTargetAnchorAbsoluteLocation;
// In case of edge with more than 2 bendpoints, we just
// compute the last segment coordinates.
if (existingPointList.size() > 2) {
sourceLineOrigin = existingPointList.getPoint(1);
targetLineOrigin = existingPointList.getPoint(existingPointList.size() - 2);
}
// In some particular cases, we could have a bendpoint list with less
// than 2 elements. In this case we create a new point list that will
// contain the two bendpoints computed from the intersection points.
else if (existingPointList.size() < 2) {
existingPointList = new PointList(new int[] { 0, 0, 0, 0 });
}
Optional<Point> sourceConnectionPoint = Optional.empty();
Optional<Point> targetConnectionPoint = Optional.empty();
if (center == CenteringStyle.BOTH || center == CenteringStyle.SOURCE) {
sourceConnectionPoint = GraphicalHelper.getIntersection(sourceLineOrigin, sourceLineTerminus, sourceBounds, false);
}
if (center == CenteringStyle.BOTH || center == CenteringStyle.TARGET) {
targetConnectionPoint = GraphicalHelper.getIntersection(targetLineOrigin, targetLineTerminus, targetBounds, false);
}
if (sourceConnectionPoint.isPresent() || targetConnectionPoint.isPresent()) {
if (sourceConnectionPoint.isPresent()) {
existingPointList.setPoint(sourceConnectionPoint.get(), 0);
centerSourceAnchor();
}
if (targetConnectionPoint.isPresent()) {
existingPointList.setPoint(targetConnectionPoint.get(), existingPointList.size() - 1);
centerTargetAnchor();
}
}
}
/**
* Deal with the rectilinear edge case.
*
* @param center
* the {@link CenteringStyle} value:
* {@link CenteringStyle#SOURCE} {@link CenteringStyle#TARGET} or
* {@link CenteringStyle#BOTH}.
* @param sourceBounds
* the source figure bounds.
* @param targetBounds
* the target figure bounds.
* @param existingPointList
* the existing point list before centering any connection end.
*/
private void handleRectilinearCase(CenteringStyle center, Rectangle sourceBounds, Rectangle targetBounds, PointList existingPointList) {
PointList rectilinear = null;
// If the connection is available (the edge already exist) we retrieve
// the rectilinear bendpoints.
if (connection != null) {
rectilinear = getRectilinearPointListFromConnection();
} else {
rectilinear = existingPointList.getCopy();
// GMF bendpoints are not reliable: in some cases they are
// completely far away from what draw2D is really displaying. If
// there is only two GMF bendpoints, we can compute them with the
// information we know. We also could have a point list with less
// than two elements.
if (rectilinear.size() <= 2) {
computePointListByIntersections(rectilinear, sourceBounds, targetBounds);
}
if (rectilinear.size() >= 2) {
CanonicalRectilinearRouter rectilinearRouter = new CanonicalRectilinearRouter();
rectilinearRouter.routeLine(edge, 0, rectilinear, sourceBounds, targetBounds);
}
}
if (rectilinear.size() >= 2) {
RectilinearEdgeUtil.centerEdgeEnds(rectilinear, newSourceAnchorAbsoluteLocation, newTargetAnchorAbsoluteLocation, center);
if (center == CenteringStyle.BOTH || center == CenteringStyle.SOURCE) {
centerSourceAnchor();
}
if (center == CenteringStyle.BOTH || center == CenteringStyle.TARGET) {
centerTargetAnchor();
}
existingPointList.removeAllPoints();
existingPointList.addAll(rectilinear);
}
}
private void computePointListByIntersections(PointList rectilinear, Rectangle sourceBounds, Rectangle targetBounds) {
Optional<Point> sourceConnectionPoint = GraphicalHelper.getIntersection(existingSourceAnchorAbsoluteLocation, existingTargetAnchorAbsoluteLocation, sourceBounds, false);
Optional<Point> targetConnectionPoint = GraphicalHelper.getIntersection(existingSourceAnchorAbsoluteLocation, existingTargetAnchorAbsoluteLocation, targetBounds, false);
if (sourceConnectionPoint.isPresent() && targetConnectionPoint.isPresent()) {
rectilinear.removeAllPoints();
rectilinear.addPoint(sourceConnectionPoint.get());
rectilinear.addPoint(targetConnectionPoint.get());
}
}
private void computeAndSetNewAnchorsAbsoluteCoordinates(Rectangle sourceBounds, Rectangle targetBounds, CenteringStyle center) {
newSourceAnchorAbsoluteLocation = existingSourceAnchorAbsoluteLocation;
newTargetAnchorAbsoluteLocation = existingTargetAnchorAbsoluteLocation;
// if the source is centered, we compute the new anchor coordinates
// source anchor
if (center == CenteringStyle.BOTH || center == CenteringStyle.SOURCE) {
PrecisionPoint newSourceAnchor = new PrecisionPoint(0.5, 0.5);
newSourceAnchorAbsoluteLocation = getAbsoluteAnchorCoordinates(sourceBounds, newSourceAnchor);
}
// if the target is centered, we compute the new anchor coordinates
if (center == CenteringStyle.BOTH || center == CenteringStyle.TARGET) {
PrecisionPoint newTargetAnchor = new PrecisionPoint(0.5, 0.5);
newTargetAnchorAbsoluteLocation = getAbsoluteAnchorCoordinates(targetBounds, newTargetAnchor);
}
}
private void retrieveAndSetExistingAnchorsAbsoluteLocation(Rectangle sourceBounds, Rectangle targetBounds) {
existingSourceAnchorAbsoluteLocation = getAbsoluteAnchorCoordinates(sourceBounds, getAnchorId(edge.getSourceAnchor()));
existingTargetAnchorAbsoluteLocation = getAbsoluteAnchorCoordinates(targetBounds, getAnchorId(edge.getTargetAnchor()));
}
private PrecisionPoint getAnchorId(Anchor anchor) {
PrecisionPoint anchorId = new PrecisionPoint(0.5, 0.5);
if (anchor instanceof IdentityAnchor) {
anchorId = BaseSlidableAnchor.parseTerminalString(((IdentityAnchor) anchor).getId());
}
return anchorId;
}
/**
* Set the source anchor at (0.5, 0.5).
*/
private void centerSourceAnchor() {
Anchor currentAnchor = edge.getSourceAnchor();
IdentityAnchor newAnchor = setOrCreateNewAnchorIfNeeded(currentAnchor);
if (newAnchor != currentAnchor) {
edge.setSourceAnchor(newAnchor);
}
}
/**
* Set the target anchor at (0.5, 0.5).
*/
private void centerTargetAnchor() {
Anchor currentAnchor = edge.getTargetAnchor();
IdentityAnchor newAnchor = setOrCreateNewAnchorIfNeeded(currentAnchor);
if (newAnchor != currentAnchor) {
edge.setTargetAnchor(newAnchor);
}
}
private IdentityAnchor setOrCreateNewAnchorIfNeeded(Anchor anchor) {
if (anchor instanceof IdentityAnchor) {
if (!((IdentityAnchor) anchor).getId().equals(CENTERED_ANCHOR)) {
((IdentityAnchor) anchor).setId(CENTERED_ANCHOR);
}
return (IdentityAnchor) anchor;
} else {
IdentityAnchor a = NotationFactory.eINSTANCE.createIdentityAnchor();
a.setId(CENTERED_ANCHOR);
return a;
}
}
/**
* Compute the anchor absolute coordinates following the figure bounds and
* the anchor location.
*
* @param absoluteBounds
* the figure absolute bounds.
* @param precisionPoint
* the anchor location.
* @return the anchor absolute location.
*/
private PrecisionPoint getAbsoluteAnchorCoordinates(Rectangle absoluteBounds, PrecisionPoint precisionPoint) {
return new PrecisionPoint(absoluteBounds.x() + (absoluteBounds.width() * precisionPoint.preciseX()), absoluteBounds.y() + (absoluteBounds.height() * precisionPoint.preciseY()));
}
private boolean isEdgeSourceCentered() {
return isCentered(true);
}
private boolean isEdgeTargetCentered() {
return isCentered(false);
}
private boolean isCentered(boolean source) {
boolean centered = false;
EObject element = edge.getElement();
if (element instanceof DEdge) {
EdgeStyle edgeStyle = ((DEdge) element).getOwnedStyle();
CenteringStyle centeringStyle = edgeStyle.getCentered();
if (source) {
centered = centeringStyle == CenteringStyle.BOTH || centeringStyle == CenteringStyle.SOURCE;
} else {
centered = centeringStyle == CenteringStyle.BOTH || centeringStyle == CenteringStyle.TARGET;
}
}
return centered;
}
/**
* We try to retrieve the edge connection figure.
*/
@SuppressWarnings("rawtypes")
private void setConnectionIfNull() {
if (connection != null || !useFigure) {
return;
}
Option<GraphicalEditPart> option = Options.newNone();
final IEditorPart editorPart = EclipseUIUtil.getActiveEditor();
if (editorPart instanceof DiagramEditor) {
option = GMFHelper.getGraphicalEditPart(edge, (DiagramEditor) editorPart);
}
// If the active editor is not a DiagramEditor, that means the focus is
// on a VSM or an other editor that triggered this operation
else {
List diagramEditors = EditorService.getInstance().getRegisteredEditorParts();
for (Iterator it = diagramEditors.iterator(); it.hasNext();) {
Object object = it.next();
if (object instanceof DiagramEditor) {
option = GMFHelper.getGraphicalEditPart(edge, (DiagramEditor) object);
}
if (option.some()) {
break;
}
}
}
if (option.some()) {
GraphicalEditPart editPart = option.get();
if (editPart instanceof DEdgeEditPart) {
connectionEditPart = (DEdgeEditPart) editPart;
connection = ((DEdgeEditPart) editPart).getConnectionFigure();
}
}
}
/**
* In the case of an existing edge that was oblique and is now transformed
* to rectilinear, we need to anticipate the future rectilinear bendpoints
* to next center the source or target connection ends.
*
* <strong>We suppose the connection attribute is not null</strong>
*
* @return a rectilinear PointList.
*/
private PointList getRectilinearPointListFromConnection() {
ConnectionRouter oldConnectionRouter = connection.getConnectionRouter();
boolean needToRetrieveOldRouter = false;
// If the router is already a rectilinear one, we don't need to change
// it.
if (!(oldConnectionRouter instanceof RectilinearRouter)) {
ConnectionRouter connectionRouter = null;
LayerManager layerManager = LayerManager.Helper.find(connectionEditPart);
if (layerManager != null) {
ConnectionLayer cLayer = (ConnectionLayer) layerManager.getLayer(LayerConstants.CONNECTION_LAYER);
if (cLayer instanceof ConnectionLayerEx) {
connectionRouter = ((ConnectionLayerEx) cLayer).getRectilinearRouter();
}
}
if (connectionRouter == null) {
connectionRouter = new RectilinearRouter();
}
connection.setConnectionRouter(connectionRouter);
needToRetrieveOldRouter = true;
}
connection.getConnectionRouter().route(connection);
PointList pointList = connection.getPoints().getCopy();
// We restore the old router once we got the rectilinear point
// list.
if (needToRetrieveOldRouter) {
connection.setConnectionRouter(oldConnectionRouter);
connection.getConnectionRouter().route(connection);
}
return pointList;
}
}