blob: af7dce3696fb7d304723ae08dafcb47a69e998bb [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014, 2020 THALES GLOBAL SERVICES.
* 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
*******************************************************************************/
package org.eclipse.sirius.diagram.ui.internal.refresh.listeners;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.eclipse.draw2d.Connection;
import org.eclipse.draw2d.ConnectionRouter;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.transaction.RecordingCommand;
import org.eclipse.emf.transaction.TransactionalEditingDomain;
import org.eclipse.gef.GraphicalEditPart;
import org.eclipse.gmf.runtime.common.ui.services.editor.EditorService;
import org.eclipse.gmf.runtime.diagram.ui.parts.DiagramEditor;
import org.eclipse.gmf.runtime.draw2d.ui.internal.routers.RectilinearRouter;
import org.eclipse.gmf.runtime.notation.Diagram;
import org.eclipse.gmf.runtime.notation.Edge;
import org.eclipse.gmf.runtime.notation.Location;
import org.eclipse.gmf.runtime.notation.Node;
import org.eclipse.gmf.runtime.notation.NotationPackage;
import org.eclipse.gmf.runtime.notation.RelativeBendpoints;
import org.eclipse.gmf.runtime.notation.RoutingStyle;
import org.eclipse.gmf.runtime.notation.View;
import org.eclipse.sirius.business.api.session.ModelChangeTrigger;
import org.eclipse.sirius.business.api.session.Session;
import org.eclipse.sirius.business.api.session.SessionEventBroker;
import org.eclipse.sirius.common.ui.tools.api.util.EclipseUIUtil;
import org.eclipse.sirius.diagram.DDiagram;
import org.eclipse.sirius.diagram.DEdge;
import org.eclipse.sirius.diagram.DNode;
import org.eclipse.sirius.diagram.DiagramPackage;
import org.eclipse.sirius.diagram.EdgeStyle;
import org.eclipse.sirius.diagram.ui.business.api.query.EdgeQuery;
import org.eclipse.sirius.diagram.ui.business.api.view.SiriusGMFHelper;
import org.eclipse.sirius.diagram.ui.business.internal.operation.AbstractModelChangeOperation;
import org.eclipse.sirius.diagram.ui.internal.edit.parts.DEdgeEditPart;
import org.eclipse.sirius.diagram.ui.internal.operation.CenterEdgeEndModelChangeOperation;
import org.eclipse.sirius.diagram.ui.internal.operation.RemoveBendpointsOperation;
import org.eclipse.sirius.diagram.ui.internal.refresh.GMFHelper;
import org.eclipse.sirius.diagram.ui.tools.internal.routers.DForestRouter;
import org.eclipse.sirius.ext.base.Option;
import org.eclipse.sirius.ext.base.Options;
import org.eclipse.sirius.viewpoint.Style;
import org.eclipse.sirius.viewpoint.ViewpointPackage;
import org.eclipse.ui.IEditorPart;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
/**
* A Model Change Trigger that execute the {@link CenterEdgeEndModelChangeOperation} when features defined in
* {@link RefreshEdgeLayoutScopePredicate} are updated.
*
* @author Florian Barbin
*/
@SuppressWarnings("restriction")
public class EdgeLayoutUpdaterModelChangeTrigger implements ModelChangeTrigger {
public static final int PRIORITY = FilterListener.COMPOSITE_FILTER_REFRESH_PRIORITY + 1;
/**
* List of features for which we consider that the edge layout must be recompute.
*/
private static final Set<EStructuralFeature> REFRESH_FEATURES = new HashSet<EStructuralFeature>();
/**
* Sublist of <code>REFRESH_FEATURES</code> that also have other features as consequence.
*/
private static final Set<EStructuralFeature> REFRESH_FEATURES_WITH_CONSEQUENCE = new HashSet<EStructuralFeature>();
/**
* List of features that are standard consequences of <code>REFRESH_FEATURES_WITH_CONSEQUENCE</code>.
*/
private static final Set<EStructuralFeature> CONSEQUENCE_FEATURES = new HashSet<EStructuralFeature>();
/**
* List of features for which we consider that the edge bendpoints must be recompute after change into oblique edge.
*/
private static final Set<EStructuralFeature> ROUTING_FEATURES = new HashSet<EStructuralFeature>();
/**
* List of features concerning move or resize of the source/target of an edge.
*/
private static final Set<EStructuralFeature> MOVE_OR_RESIZE_FEATURES = new HashSet<EStructuralFeature>();
static {
REFRESH_FEATURES_WITH_CONSEQUENCE.add(DiagramPackage.Literals.EDGE_STYLE__CENTERED);
REFRESH_FEATURES_WITH_CONSEQUENCE.add(NotationPackage.Literals.ROUTING_STYLE__ROUTING);
REFRESH_FEATURES.addAll(REFRESH_FEATURES_WITH_CONSEQUENCE);
REFRESH_FEATURES.add(DiagramPackage.Literals.DEDGE__OWNED_STYLE);
REFRESH_FEATURES.add(NotationPackage.Literals.DIAGRAM__PERSISTED_EDGES);
CONSEQUENCE_FEATURES.add(ViewpointPackage.Literals.CUSTOMIZABLE__CUSTOM_FEATURES);
CONSEQUENCE_FEATURES.add(DiagramPackage.Literals.EDGE_STYLE__ROUTING_STYLE);
ROUTING_FEATURES.add(DiagramPackage.Literals.EDGE_STYLE__ROUTING_STYLE);
ROUTING_FEATURES.add(NotationPackage.Literals.ROUTING_STYLE__ROUTING);
MOVE_OR_RESIZE_FEATURES.add(NotationPackage.Literals.LOCATION__X);
MOVE_OR_RESIZE_FEATURES.add(NotationPackage.Literals.LOCATION__Y);
MOVE_OR_RESIZE_FEATURES.add(NotationPackage.Literals.SIZE__WIDTH);
MOVE_OR_RESIZE_FEATURES.add(NotationPackage.Literals.SIZE__HEIGHT);
MOVE_OR_RESIZE_FEATURES.add(NotationPackage.Literals.NODE__LAYOUT_CONSTRAINT);
MOVE_OR_RESIZE_FEATURES.add(NotationPackage.Literals.EDGE__BENDPOINTS);
}
private TransactionalEditingDomain domain;
private SessionEventBroker eventBroker;
private RefreshEdgeLayoutNotificationFilter refreshEdgeLayoutNotificationFilter;
/**
* Constructor. Add this EdgeLayoutUpdaterModelChangeTrigger to the session event broker of the given session.
*
* @param domain
* the editing domain.
* @param session
* the session.
* @param dDiagram
* the ddiagram.
*/
public EdgeLayoutUpdaterModelChangeTrigger(Session session, DDiagram dDiagram) {
this.domain = session.getTransactionalEditingDomain();
eventBroker = session.getEventBroker();
refreshEdgeLayoutNotificationFilter = new RefreshEdgeLayoutNotificationFilter(dDiagram);
eventBroker.addLocalTrigger(refreshEdgeLayoutNotificationFilter, this);
}
@Override
public Option<Command> localChangesAboutToCommit(Collection<Notification> notifications) {
Command command = null;
// this collection contains gmf edges for which we already created a
// CenterEdgeEndModelChangeOperation. This list aims to avoid creating
// multi operation for a same gmfEdge in the case we are several
// notification for it.
Collection<Edge> edgesWithCreatedCommand = new HashSet<Edge>();
Collection<AbstractModelChangeOperation<Void>> operations = new ArrayList<AbstractModelChangeOperation<Void>>();
Map<Notification, Edge> notifToEdge = new HashMap<>();
Map<Notification, Node> notifToNode = new HashMap<>();
List<View> movedOrResizedViews = new ArrayList<>();
prepareData(notifications, notifToEdge, notifToNode, movedOrResizedViews);
for (Notification notification : notifications) {
boolean isChangedIntoOblique = changeIntoOblique(operations, notification, notifToEdge);
// Only consider notification of
// RefreshEdgeLayoutNotificationFilter.REFRESH_FEATURES list and for
// which the source or the target has not been moved
if (isRefreshEdgeLayoutNeededForNotification(notification, notifToEdge, movedOrResizedViews) && !isChangedIntoOblique) {
Optional<Edge> optionalGmfEdge = getCorrespondingEdge(notification, notifToEdge);
if (optionalGmfEdge.isPresent() && edgesWithCreatedCommand.add(optionalGmfEdge.get())) {
// if there are several notifications, we do not try to
// retrieve draw2D informations since they could be out of
// date.
boolean useFigure = otherNotificationsAreConsequences(notification, optionalGmfEdge.get(), notifications, notifToEdge);
AbstractModelChangeOperation<Void> operation = new CenterEdgeEndModelChangeOperation(optionalGmfEdge.get(), useFigure);
operations.add(operation);
}
}
}
if (!operations.isEmpty()) {
command = new EdgeLayoutUpdaterCommand(domain, operations);
}
return Options.newSome(command);
}
/**
* Change edge into oblique line if the edge should be change from rectilinear or tree style into oblique.
*
* @param operations
* list of operations to execute in the precommit listener,
* @param notification
* notification that trigger action,
* @param notifToEdge
* map to store the edge corresponding to the notification,
*
* @return true if the edge has been changed into an oblique line.
*/
private boolean changeIntoOblique(Collection<AbstractModelChangeOperation<Void>> operations, Notification notification, Map<Notification, Edge> notifToEdge) {
boolean isChangedIntoOblique = false;
if (ROUTING_FEATURES.contains(notification.getFeature())) {
Optional<Edge> optionalGmfEdge = getCorrespondingEdge(notification, notifToEdge);
if (optionalGmfEdge.isPresent()) {
// when edge routing style is changed into oblique, prepare the new point list in oblique line
DEdgeEditPart edgeEditPart = getEdgeEditPart(optionalGmfEdge.get());
if (edgeEditPart != null) {
Connection connection = edgeEditPart.getConnectionFigure();
ConnectionRouter oldConnectionRouter = connection.getConnectionRouter();
EdgeQuery edgeQuery = new EdgeQuery(optionalGmfEdge.get());
if ((oldConnectionRouter instanceof RectilinearRouter || oldConnectionRouter instanceof DForestRouter) && edgeQuery.isEdgeWithObliqueRoutingStyle()) {
RemoveBendpointsOperation removeBendpointsOperation = new RemoveBendpointsOperation(edgeEditPart);
operations.add(removeBendpointsOperation);
isChangedIntoOblique = true;
}
}
}
}
return isChangedIntoOblique;
}
/**
* Get all {@link View}s that have been moved or resized from notifications.
*
* @param notifications
* The notifications
* @param notifToEdge
* map to store the edge corresponding to the notification
* @param notifToNode
* map to store the node corresponding to the notification
* @param moveOrResizeViews
* resulting list of moved or resized views
*/
private void prepareData(Collection<Notification> notifications, Map<Notification, Edge> notifToEdge, Map<Notification, Node> notifToNode, List<View> moveOrResizeViews) {
for (Notification notification : notifications) {
if (MOVE_OR_RESIZE_FEATURES.contains(notification.getFeature())) {
getCorrespondingView(notification, notifToEdge, notifToNode).ifPresent(moveOrResizeViews::add);
}
}
}
private static final class EdgeLayoutUpdaterCommand extends RecordingCommand {
private Collection<AbstractModelChangeOperation<Void>> operations;
public EdgeLayoutUpdaterCommand(TransactionalEditingDomain domain, Collection<AbstractModelChangeOperation<Void>> operations) {
super(domain);
this.operations = operations;
}
@Override
protected void doExecute() {
for (AbstractModelChangeOperation<Void> operation : operations) {
operation.execute();
}
}
}
@Override
public int priority() {
return PRIORITY;
}
/**
* Dispose this trigger. The trigger is removed from the session event broker.
*/
public void dispose() {
refreshEdgeLayoutNotificationFilter = null;
eventBroker.removeLocalTrigger(this);
eventBroker = null;
domain = null;
}
/**
* Test whether the other notifications are consequences of the given one. For instance, in case of a manual
* modification of the Sirius routing style (from Style tab of Properties view), we also update the GMF style and we
* add the routing style within the custom features. This method aims to check whether we are in the case of an
* individual modification or a global one.
*
* @param notification
* the notification for which we are notified.
* @param gmfEdge
* the GMF edge associated to the <code>notification</code>
* @param notifications
* the whole notification list.
* @param notifToview
* Map to retrieve the View if yet computed
* @return true if the notifications list contains only notifications induced by the first one.
*/
public boolean otherNotificationsAreConsequences(final Notification notification, final Edge gmfEdge, Collection<Notification> notifications, Map<Notification, Edge> notifToEdge) {
boolean otherNotificationsAreIndirectlyConcerned = false;
if (notifications.size() == 1 && REFRESH_FEATURES.contains(notifications.iterator().next().getFeature())) {
otherNotificationsAreIndirectlyConcerned = true;
} else if (REFRESH_FEATURES_WITH_CONSEQUENCE.contains(notification.getFeature())) {
otherNotificationsAreIndirectlyConcerned = Iterables.all(notifications, new Predicate<Notification>() {
@Override
public boolean apply(Notification currentNotification) {
boolean considerAsConsequence = false;
if (currentNotification == notification) {
considerAsConsequence = true;
} else {
Optional<Edge> optionalEdge = getCorrespondingEdge(currentNotification, notifToEdge);
if (optionalEdge.isPresent()) {
considerAsConsequence = optionalEdge.get().equals(gmfEdge) && CONSEQUENCE_FEATURES.contains(currentNotification.getFeature());
}
}
return considerAsConsequence;
}
});
}
return otherNotificationsAreIndirectlyConcerned;
}
/**
* Test whether the edge centering should be refreshed for this notification.
*
* @param notification
* The {@link Notification} to check.
* @param notifToEdge
* Map to retrieve the Edge if yet computed
* @param movedOrResizedViews
* Map to retrieve the Node if yet computed
* @return true if this notification concerns the edge ends centering, false otherwise.
*/
private boolean isRefreshEdgeLayoutNeededForNotification(final Notification notification, Map<Notification, Edge> notifToEdge, List<View> movedOrResizedViews) {
if (REFRESH_FEATURES.contains(notification.getFeature())) {
Optional<Edge> optionalEdge = getCorrespondingEdge(notification, notifToEdge);
if (optionalEdge.isPresent()) {
// If one of the source or target of the edge is moved, then the refresh will be done in the context of
// the full refresh so it is not the responsibility to this ModelChangeTrigger to do it
final Edge referenceEdge = optionalEdge.get();
if (!movedOrResizedViews.contains(referenceEdge.getSource()) && !movedOrResizedViews.contains(referenceEdge.getTarget())) {
return true;
}
}
}
return false;
}
/**
* Search the corresponding GMF view associated to this notification.
*
* @param notification
* The {@link Notification} to analyze
* @param notifToEdge
* Map to retrieve the Edge if yet computed
* @param notifToNode
* Map to retrieve the Node if yet computed
* @return an optional {@link View}
*/
private Optional<? extends View> getCorrespondingView(Notification notification, Map<Notification, Edge> notifToEdge, Map<Notification, Node> notifToNode) {
Optional<? extends View> result = getCorrespondingNode(notification, notifToNode);
if (!result.isPresent()) {
result = getCorrespondingEdge(notification, notifToEdge);
}
return result;
}
/**
* Search the corresponding GMF edge associated to this notification.
*
* @param notification
* The {@link Notification} to analyze
* @param notifToEdge
* Map to retrieve the edge if yet computed
* @return an optional {@link Edge}
*/
private Optional<Edge> getCorrespondingEdge(Notification notification, Map<Notification, Edge> notifToEdge) {
if (notifToEdge.containsKey(notification)) { // value may be null
return Optional.ofNullable(notifToEdge.get(notification));
}
Edge gmfEdge = null;
Object notifier = notification.getNotifier();
if (notifier instanceof Edge) {
gmfEdge = (Edge) notifier;
} else if (notifier instanceof DEdge) {
gmfEdge = SiriusGMFHelper.getGmfEdge((DEdge) notifier);
} else if (notifier instanceof EdgeStyle) {
EObject container = ((EdgeStyle) notifier).eContainer();
if (container instanceof DEdge) {
gmfEdge = SiriusGMFHelper.getGmfEdge((DEdge) container);
}
} else if (notifier instanceof RoutingStyle) {
EObject container = ((RoutingStyle) notifier).eContainer();
if (container instanceof Edge) {
gmfEdge = ((Edge) container);
}
} else if (notifier instanceof Diagram && notification.getNewValue() instanceof Edge) {
gmfEdge = (Edge) notification.getNewValue();
} else if (notifier instanceof RelativeBendpoints) {
gmfEdge = (Edge) ((RelativeBendpoints) notifier).eContainer();
}
notifToEdge.put(notification, gmfEdge);
return Optional.ofNullable(gmfEdge);
}
/**
* Search the corresponding GMF node associated to this notification.
*
* @param notification
* The {@link Notification} to analyze
* @param notifToNode
* Map to retrieve the Node if yet computed
* @return an optional {@link Node}
*/
private Optional<Node> getCorrespondingNode(Notification notification, Map<Notification, Node> notifToNode) {
if (notifToNode.containsKey(notification)) { // value may be null
return Optional.ofNullable(notifToNode.get(notification));
}
Node gmfNode = null;
Object notifier = notification.getNotifier();
if (notifier instanceof DNode) {
gmfNode = SiriusGMFHelper.getGmfNode((DNode) notifier);
} else if (notifier instanceof Style) {
EObject container = ((Style) notifier).eContainer();
if (container instanceof DNode) {
gmfNode = SiriusGMFHelper.getGmfNode((DNode) container);
}
} else if (notifier instanceof Location) {
EObject container = ((Location) notifier).eContainer();
if (container instanceof Node) {
gmfNode = ((Node) container);
}
}
notifToNode.put(notification, gmfNode);
return Optional.ofNullable(gmfNode);
}
/**
* Get the edit part corresponding to a given edge. The edit part returned can be null, for example in case of
* creation of new edge.
*
* @param edge
* the edge with the edit part to recover
* @return the edit part corresponding to a given edge.
*/
private DEdgeEditPart getEdgeEditPart(Edge edge) {
DEdgeEditPart edgeEditPart = null;
Option<GraphicalEditPart> option = Options.newNone();
final IEditorPart editorPart = EclipseUIUtil.getActiveEditor();
if (editorPart instanceof DiagramEditor) {
option = GMFHelper.getGraphicalEditPart(edge, (DiagramEditor) editorPart);
} else {
// If the active editor is not a DiagramEditor, that means the focus is
// on a VSM or an other editor that triggered this operation
List<?> editors = EditorService.getInstance().getRegisteredEditorParts();
for (Iterator<?> it = editors.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) {
edgeEditPart = ((DEdgeEditPart) editPart);
}
}
return edgeEditPart;
}
}