diff --git a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/META-INF/MANIFEST.MF b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/META-INF/MANIFEST.MF index 185a1b97..0e6501b8 100644 --- a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/META-INF/MANIFEST.MF +++ b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/META-INF/MANIFEST.MF @@ -17,7 +17,8 @@ Require-Bundle: org.eclipse.core.runtime;bundle-version="[3.13.0,4.0.0)", org.eclipse.papyrus.uml.service.types;bundle-version="[3.1.0,4.0.0)", org.eclipse.papyrus.infra.gmfdiag.preferences;bundle-version="[3.0.0,4.0.0)", org.eclipse.papyrus.uml.diagram.css;bundle-version="[2.0.0,3.0.0)", - org.eclipse.e4.ui.css.core;bundle-version="[0.12.101,1.0.0)" + org.eclipse.e4.ui.css.core;bundle-version="[0.12.101,1.0.0)", + org.eclipse.papyrus.infra.gmfdiag.dnd;bundle-version="[1.3.0,2.0.0)" Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-ActivationPolicy: lazy Bundle-Localization: plugin diff --git a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/parts/ExecutionSpecificationEditPart.java b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/parts/ExecutionSpecificationEditPart.java index 3cef7846..c2dcda24 100644 --- a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/parts/ExecutionSpecificationEditPart.java +++ b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/parts/ExecutionSpecificationEditPart.java @@ -22,8 +22,10 @@ import org.eclipse.emf.common.notify.Notification; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.gef.DragTracker; import org.eclipse.gef.EditPart; import org.eclipse.gef.EditPolicy; +import org.eclipse.gef.Request; import org.eclipse.gmf.runtime.diagram.ui.editparts.BorderedBorderItemEditPart; import org.eclipse.gmf.runtime.diagram.ui.editparts.IBorderItemEditPart; import org.eclipse.gmf.runtime.diagram.ui.editpolicies.EditPolicyRoles; @@ -37,6 +39,7 @@ import org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.edit.policies.ExecutionSpecificationGraphicalNodeEditPolicy; import org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.edit.policies.InteractionSemanticEditPolicy; import org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.locators.ExecutionSpecificationBorderItemLocator; +import org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.tools.SequenceDragTracker; import org.eclipse.papyrus.uml.interaction.model.MInteraction; import org.eclipse.papyrus.uml.interaction.model.MLifeline; @@ -144,4 +147,16 @@ private void refreshEditPartOfShape(Shape shape) { } } + // The superclass does not delegate to edit-policies to get the drag tracker + @Override + public DragTracker getDragTracker(Request request) { + // We support dragging and dropping onto another lifeline + return new SequenceDragTracker(this) { + + @Override + protected boolean isMove() { + return super.isMove(); + } + }; + } } diff --git a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/ExecutionSpecificationDragEditPolicy.java b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/ExecutionSpecificationDragEditPolicy.java index 7c8b5e6e..94abe66a 100644 --- a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/ExecutionSpecificationDragEditPolicy.java +++ b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/ExecutionSpecificationDragEditPolicy.java @@ -15,26 +15,47 @@ import static org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.edit.policies.PrivateRequestUtils.isAllowSemanticReordering; import static org.eclipse.papyrus.uml.interaction.internal.model.commands.DependencyContext.defer; import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.as; +import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.filter; +import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.flatMapToInt; +import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.flatMapToObj; +import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.mapToInt; +import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.OptionalInt; import java.util.function.Supplier; +import org.eclipse.draw2d.IFigure; +import org.eclipse.draw2d.geometry.Dimension; +import org.eclipse.draw2d.geometry.Point; +import org.eclipse.draw2d.geometry.PrecisionRectangle; import org.eclipse.draw2d.geometry.Rectangle; +import org.eclipse.gef.EditPart; import org.eclipse.gef.GraphicalEditPart; +import org.eclipse.gef.GraphicalViewer; import org.eclipse.gef.Request; import org.eclipse.gef.commands.Command; +import org.eclipse.gef.commands.UnexecutableCommand; import org.eclipse.gef.requests.ChangeBoundsRequest; import org.eclipse.gef.tools.ResizeTracker; +import org.eclipse.gmf.runtime.diagram.ui.editparts.IBorderItemEditPart; import org.eclipse.gmf.runtime.diagram.ui.editparts.IGraphicalEditPart; +import org.eclipse.gmf.runtime.diagram.ui.figures.IBorderItemLocator; import org.eclipse.gmf.runtime.notation.Node; +import org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.edit.parts.LifelineBodyEditPart; +import org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.locators.OnLineBorderItemLocator; import org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.tools.SequenceResizeTracker; import org.eclipse.papyrus.uml.interaction.model.CreationCommand; +import org.eclipse.papyrus.uml.interaction.model.MElement; import org.eclipse.papyrus.uml.interaction.model.MExecution; +import org.eclipse.papyrus.uml.interaction.model.MInteraction; +import org.eclipse.papyrus.uml.interaction.model.MLifeline; import org.eclipse.papyrus.uml.interaction.model.MMessageEnd; import org.eclipse.papyrus.uml.interaction.model.MOccurrence; import org.eclipse.uml2.uml.Element; import org.eclipse.uml2.uml.ExecutionOccurrenceSpecification; +import org.eclipse.uml2.uml.InteractionFragment; import org.eclipse.uml2.uml.OccurrenceSpecification; /** @@ -61,7 +82,7 @@ protected MExecution getExecution() { } @Override - protected Command getSetBoundsCommand(ChangeBoundsRequest request, Node execShape, Rectangle newBounds) { + protected Command getResizeCommand(ChangeBoundsRequest request, Node execShape, Rectangle newBounds) { org.eclipse.emf.common.command.Command result = null; MExecution execution = getExecution(); @@ -143,4 +164,165 @@ protected Supplier move(MExecu protected ResizeTracker getResizeTracker(int direction) { return new SequenceResizeTracker((GraphicalEditPart)getHost(), direction); } + + @Override + protected Command getMoveCommand(ChangeBoundsRequest request, Node execShape, Rectangle newBounds) { + org.eclipse.emf.common.command.Command result = null; + + MExecution execution = getExecution(); + + // Check for semantic re-ordering + if (!isAllowSemanticReordering(request) + && impliesFragmentReordering(execution, execShape, newBounds)) { + return UnexecutableCommand.INSTANCE; + } + + OptionalInt top = execution.getTop(); + OptionalInt bottom = execution.getBottom(); + + int absTop = getLayoutHelper().toAbsoluteY(execShape, newBounds.y()); + int absBottom = getLayoutHelper().toAbsoluteY(execShape, newBounds.bottom()); + + if (top.isPresent() && (top.getAsInt() != absTop) && bottom.isPresent() + && (bottom.getAsInt() != absBottom)) { + + result = getMoveExecutionCommand(request, execution, absTop, absBottom); + } + + return wrap(result); + } + + protected boolean impliesFragmentReordering(MExecution execution, Node execShape, Rectangle newBounds) { + MInteraction interaction = execution.getInteraction(); + List fragments = interaction.getElement().getFragments(); + + // Get the element above that is being moved and could be re-ordered + Optional> normalizedStart = execution.getStart(); + Optional startEnd = as(normalizedStart, MMessageEnd.class); + if (startEnd.filter(MMessageEnd::isReceive).isPresent()) { + // Take the send end + normalizedStart = startEnd.flatMap(MMessageEnd::getOtherEnd); + } + OptionalInt startIndex = filter( + mapToInt(normalizedStart, start -> fragments.indexOf(start.getElement())), i -> i > 0); + Optional> above = flatMapToObj(startIndex, + i -> interaction.getElement(fragments.get(i - 1))); + + // Get the element below that is being moved and could be re-ordered + Optional> normalizedFinish = execution.getFinish(); + Optional finishEnd = as(normalizedFinish, MMessageEnd.class); + if (finishEnd.filter(MMessageEnd::isSend).isPresent()) { + // Take the receive end + normalizedFinish = finishEnd.flatMap(MMessageEnd::getOtherEnd); + } + OptionalInt finishIndex = filter( + mapToInt(normalizedFinish, finish -> fragments.indexOf(finish.getElement())), + i -> i < (fragments.size() - 1)); + Optional> below = flatMapToObj(finishIndex, + i -> interaction.getElement(fragments.get(i + 1))); + + int absNewTop = getLayoutHelper().toAbsoluteY(execShape, newBounds.y()); + int absNewBottom = getLayoutHelper().toAbsoluteY(execShape, newBounds.bottom()); + return filter(flatMapToInt(above, MElement::getBottom), b -> b >= absNewTop).isPresent() + || filter(flatMapToInt(below, MElement::getTop), t -> t <= absNewBottom).isPresent(); + } + + protected org.eclipse.emf.common.command.Command getMoveExecutionCommand(ChangeBoundsRequest request, + MExecution execution, int top, int bottom) { + + org.eclipse.emf.common.command.Command result = null; + + // TODO: Handle changing lifeline + MLifeline newOwner = execution.getOwner(); + + if (isAllowSemanticReordering(request)) { + // TODO: handle semantic reordering + } + + result = chain(result, execution.setOwner(newOwner, OptionalInt.of(top), OptionalInt.of(bottom))); + + return result; + } + + @Override + protected void showChangeBoundsFeedback(ChangeBoundsRequest request) { + Point moveDelta = request.getMoveDelta(); + Dimension sizeDelta = request.getSizeDelta(); + + if ((sizeDelta != null) && ((sizeDelta.width != 0) || (sizeDelta.height != 0))) { + // If there's a resize involved, then don't support dropping on another lifeline + showChangeBoundsFeedback(request.getMoveDelta(), sizeDelta); + return; + } + + LifelineBodyEditPart thisLifeline = null; + for (EditPart parent = getHost().getParent(); (thisLifeline == null) + && (parent != null); parent = parent.getParent()) { + if (parent instanceof LifelineBodyEditPart) { + thisLifeline = (LifelineBodyEditPart)parent; + } + } + if (thisLifeline == null) { + showChangeBoundsFeedback(moveDelta, sizeDelta); + return; + } + + Point pointer = request.getLocation(); + EditPart dropLifeline = ((GraphicalViewer)getHost().getViewer()).findObjectAtExcluding(pointer, + Collections.singleton(thisLifeline.getFigure()), LifelineBodyEditPart.class::isInstance); + if (!(dropLifeline instanceof LifelineBodyEditPart)) { + // Not dropping on some other lifeline + showChangeBoundsFeedback(moveDelta, sizeDelta); + return; + } + + // Create a border-item locator for the lifeline we're dropping on + IFigure dropLifelineFigure = ((LifelineBodyEditPart)dropLifeline).getFigure(); + IBorderItemLocator borderItemLocator = new OnLineBorderItemLocator(dropLifelineFigure); + + // Constrain the lifeline drop to the original Y location + moveDelta = moveDelta.getCopy(); + moveDelta.setY(0); + + showChangeBoundsFeedback(moveDelta, null, dropLifelineFigure, borderItemLocator); + } + + protected void showChangeBoundsFeedback(Point moveDelta, Dimension sizeDelta) { + IBorderItemEditPart borderItemEP = (IBorderItemEditPart)getHost(); + IBorderItemLocator borderItemLocator = borderItemEP.getBorderItemLocator(); + + if (borderItemLocator != null) { + showChangeBoundsFeedback(moveDelta, sizeDelta, + ((GraphicalEditPart)getHost().getParent()).getFigure(), borderItemLocator); + } + } + + protected void showChangeBoundsFeedback(Point moveDelta, Dimension sizeDelta, IFigure onLifeline, + IBorderItemLocator locator) { + + if (locator != null) { + IBorderItemEditPart borderItemEP = (IBorderItemEditPart)getHost(); + IFigure feedback = getDragSourceFeedbackFigure(); + PrecisionRectangle rect = new PrecisionRectangle(getInitialFeedbackBounds().getCopy()); + getHostFigure().getParent().translateToAbsolute(rect); + + rect.translate(moveDelta); + if (sizeDelta != null) { + rect.resize(sizeDelta); + } + + // And bring it into the lifeline's coördinate space + Rectangle lifelineBounds = onLifeline.getBounds().getCopy(); + onLifeline.getParent().translateToAbsolute(lifelineBounds); + rect.translate(lifelineBounds.getLocation().getNegated()); + + IFigure borderItemfigure = borderItemEP.getFigure(); + Rectangle realLocation = locator.getValidLocation(rect.getCopy(), borderItemfigure); + onLifeline.translateToAbsolute(realLocation); + + feedback.translateToRelative(realLocation); + feedback.setBounds(realLocation); + } + } + } diff --git a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/LifelineBodyDropEditPolicy.java b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/LifelineBodyDropEditPolicy.java new file mode 100644 index 00000000..719deeb3 --- /dev/null +++ b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/LifelineBodyDropEditPolicy.java @@ -0,0 +1,81 @@ +/***************************************************************************** + * Copyright (c) 2018 Christian W. Damus 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: + * Christian W. Damus - Initial API and implementation + *****************************************************************************/ + +package org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.edit.policies; + +import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.as; + +import java.util.Optional; +import java.util.OptionalInt; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.gef.commands.Command; +import org.eclipse.gef.commands.UnexecutableCommand; +import org.eclipse.gmf.runtime.diagram.ui.editpolicies.DragDropEditPolicy; +import org.eclipse.gmf.runtime.diagram.ui.requests.DropObjectsRequest; +import org.eclipse.papyrus.uml.interaction.model.MExecution; +import org.eclipse.papyrus.uml.interaction.model.MLifeline; +import org.eclipse.papyrus.uml.interaction.model.util.Optionals; +import org.eclipse.uml2.uml.ExecutionSpecification; +import org.eclipse.uml2.uml.Lifeline; + +/** + * Edit policy for dropping elements onto the lifeline body. + */ +public class LifelineBodyDropEditPolicy extends DragDropEditPolicy implements ISequenceEditPolicy { + + /** + * Initializes me. + */ + public LifelineBodyDropEditPolicy() { + super(); + } + + @Override + protected Command getDropElementCommand(EObject element, DropObjectsRequest request) { + Command result = null; + + if (element instanceof ExecutionSpecification) { + Optional exec = Optionals + .as(getInteraction().getElement((ExecutionSpecification)element), MExecution.class); + return exec.map(e -> getDropExecutionCommand(e, request)).orElse(null); + } + + return result; + } + + /** + * Obtain a command that drops an {@code execution} specification onto the host lifeline. + * + * @param execution + * an execution specification to drop + * @param request + * the drop request + * @return the command, or {@code null} if none + */ + protected Command getDropExecutionCommand(MExecution execution, DropObjectsRequest request) { + Optional lifeline = getHostLifeline(); + + if (!PrivateRequestUtils.isAllowSemanticReordering(request)) { + // Block the operation if the semantic override modifier is not applied + return UnexecutableCommand.INSTANCE; + } + + return lifeline.map(ll -> wrap(execution.setOwner(ll, OptionalInt.empty(), OptionalInt.empty()))) + .orElse(null); + } + + protected Optional getHostLifeline() { + Optional lifeline = as(Optional.ofNullable(getHostObject()), Lifeline.class); + return lifeline.flatMap(getInteraction()::getLifeline); + } +} diff --git a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/NoPapyrusEditPolicyProvider.java b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/NoPapyrusEditPolicyProvider.java index 20519693..02148eb1 100644 --- a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/NoPapyrusEditPolicyProvider.java +++ b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/NoPapyrusEditPolicyProvider.java @@ -19,13 +19,16 @@ import org.eclipse.gef.EditPart; import org.eclipse.gef.EditPolicy; +import org.eclipse.gef.requests.ChangeBoundsRequest; import org.eclipse.gmf.runtime.common.core.service.AbstractProvider; import org.eclipse.gmf.runtime.common.core.service.IOperation; import org.eclipse.gmf.runtime.diagram.ui.editpolicies.EditPolicyRoles; +import org.eclipse.gmf.runtime.diagram.ui.requests.DropObjectsRequest; import org.eclipse.gmf.runtime.diagram.ui.services.editpolicy.CreateEditPoliciesOperation; import org.eclipse.gmf.runtime.diagram.ui.services.editpolicy.IEditPolicyProvider; import org.eclipse.gmf.runtime.notation.View; import org.eclipse.papyrus.infra.gmfdiag.common.helper.NotationHelper; +import org.eclipse.papyrus.infra.gmfdiag.dnd.policy.CustomizableDropEditPolicy; import org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.Activator; import org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.edit.parts.ExecutionSpecificationEditPart; import org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.edit.parts.InteractionCompartmentEditPart; @@ -53,7 +56,7 @@ public NoPapyrusEditPolicyProvider() { // Creation edit policies substitute(LifelineBodyEditPart.class, EditPolicyRoles.CREATION_ROLE, - LifelineCreationEditPolicy::new); + withDrop(LifelineCreationEditPolicy::new, LifelineBodyDropEditPolicy::new)); substitute(InteractionCompartmentEditPart.class, EditPolicyRoles.CREATION_ROLE, InteractionCreationEditPolicy::new); substitute(ExecutionSpecificationEditPart.class, EditPolicyRoles.CREATION_ROLE, @@ -132,8 +135,45 @@ private boolean shouldRemovePolicy(EditPolicy policy) { return false; } - // - // Nested types - // + /** + * Compose {@code create} and {@code drop} edit-policies into a Papyrus-compliant customizable drop + * edit-policy that is installed in the {@linkplain EditPolicyRoles#CREATION_ROLE creation role}. + * + * @param create + * supplier of a create edit-policy + * @param drop + * supplier of a drop edit-policy + * @return the composed edit-policy + */ + static Supplier withDrop(Supplier create, + Supplier drop) { + return () -> { + EditPolicy createPolicy = create.get(); + EditPolicy dropPolicy = drop.get(); + + if (dropPolicy == null) { + return createPolicy; + } + + return new CustomizableDropEditPolicy(dropPolicy, createPolicy) { + @Override + public void setHost(EditPart host) { + defaultCreationEditPolicy.setHost(host); + defaultDropEditPolicy.setHost(host); + + super.setHost(host); + } + + @Override + protected DropObjectsRequest castToDropObjectsRequest(ChangeBoundsRequest request) { + DropObjectsRequest result = super.castToDropObjectsRequest(request); + + PrivateRequestUtils.forwardParameters(request, result); + + return result; + } + }; + }; + } } diff --git a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/PrivateRequestUtils.java b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/PrivateRequestUtils.java index 203ecb9f..cec7c6e2 100644 --- a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/PrivateRequestUtils.java +++ b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/PrivateRequestUtils.java @@ -13,6 +13,7 @@ package org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.edit.policies; import java.util.Map; +import java.util.stream.Stream; import org.eclipse.draw2d.geometry.Point; import org.eclipse.gef.Request; @@ -31,6 +32,12 @@ public final class PrivateRequestUtils { private static final String ALLOW_SEMANTIC_REORDERING_PARAMETER = "__allow_semantic_reordering__"; //$NON-NLS-1$ + private static final String[] PARAMETERS = { // + FORCE_PARAMETER, // + ORIGINAL_MOUSE_PARAMETER, ORIGINAL_SOURCE_PARAMETER, ORIGINAL_TARGET_PARAMETER, // + ALLOW_SEMANTIC_REORDERING_PARAMETER, // + }; + /** * Not instantiable by clients. */ @@ -159,6 +166,11 @@ static boolean getBoolean(Request request, Object parameterKey) { return getParameter(request, parameterKey, Boolean.class, Boolean.FALSE).booleanValue(); } + static boolean hasParameter(Request request, Object parameterKey) { + Map parameters = request.getExtendedData(); + return parameters.containsKey(parameterKey); + } + static T getParameter(Request request, Object parameterKey, Class type, T defaultValue) { Map parameters = request.getExtendedData(); Object value = parameters.get(parameterKey); @@ -170,4 +182,20 @@ static void setParameter(Request request, Object parameterKey, Object value) { Map parameters = request.getExtendedData(); parameters.put(parameterKey, value); } + + /** + * Forward request parameters from one request to another. Only private parameters managed by this utility + * are forwarded that the "from" request actually bears, and then only those that are not already defined + * in the "to" request. + * + * @param fromRequest + * the request bearing parameters + * @param toRequest + * a request to which to forward them + */ + public static void forwardParameters(Request fromRequest, Request toRequest) { + Stream.of(PARAMETERS).filter(p -> hasParameter(fromRequest, p)) + .filter(p -> !hasParameter(toRequest, p)) + .forEach(p -> setParameter(toRequest, p, getParameter(fromRequest, p, Object.class, null))); + } } diff --git a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/ResizableBorderItemPolicy.java b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/ResizableBorderItemPolicy.java index 4de5b50d..33a04bbf 100644 --- a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/ResizableBorderItemPolicy.java +++ b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/ResizableBorderItemPolicy.java @@ -69,11 +69,11 @@ protected Command getResizeCommand(ChangeBoundsRequest request) { Rectangle newBounds = bounds.translate(scaledMovedDelta).resize(scaledSizeDelta); - return getSetBoundsCommand(request, (Node)shapeView, newBounds); + return getResizeCommand(request, (Node)shapeView, newBounds); } /** - * Get a command to change the bounds of the given execution specification shape. + * Get a command to change the bounds of the given execution specification shape on resize. * * @param request * the change-bounds request @@ -83,7 +83,54 @@ protected Command getResizeCommand(ChangeBoundsRequest request) { * the new bounds requested for the execution specification * @return the command */ - protected Command getSetBoundsCommand(ChangeBoundsRequest request, Node execShape, Rectangle newBounds) { + protected Command getResizeCommand(ChangeBoundsRequest request, Node execShape, Rectangle newBounds) { + TransactionalEditingDomain editingDomain = ((IGraphicalEditPart)getHost()).getEditingDomain(); + ICommand result = new SetBoundsCommand(editingDomain, + DiagramUIMessages.SetLocationCommand_Label_Resize, new EObjectAdapter(execShape), newBounds); + return new ICommandProxy(result); + } + + @Override + protected Command getMoveCommand(ChangeBoundsRequest request) { + IFigure figure = getHostFigure(); + if (figure == null) { + return super.getMoveCommand(request); + } + + View shapeView = NotationHelper.findView(getHost()); + Integer x = (Integer)ViewUtil.getStructuralFeatureValue(shapeView, + NotationPackage.eINSTANCE.getLocation_X()); + Integer y = (Integer)ViewUtil.getStructuralFeatureValue(shapeView, + NotationPackage.eINSTANCE.getLocation_Y()); + Integer width = (Integer)ViewUtil.getStructuralFeatureValue(shapeView, + NotationPackage.eINSTANCE.getSize_Width()); + Integer height = (Integer)ViewUtil.getStructuralFeatureValue(shapeView, + NotationPackage.eINSTANCE.getSize_Height()); + + Rectangle bounds = new Rectangle(x.intValue(), y.intValue(), width.intValue(), height.intValue()); + + // apply transformation with scaling to get new bounds (missing from getTrasnformedRectangle) + double scale = FigureUtils.getScale(getHostFigure()); + Point scaledMovedDelta = request.getMoveDelta().getCopy(); + scaledMovedDelta.performScale(1 / scale); + + Rectangle newBounds = bounds.translate(scaledMovedDelta); + + return getMoveCommand(request, (Node)shapeView, newBounds); + } + + /** + * Get a command to change the bounds of the given execution specification shape on move. + * + * @param request + * the change-bounds request + * @param execShape + * the execution specification shape to change + * @param newBounds + * the new bounds requested for the execution specification + * @return the command + */ + protected Command getMoveCommand(ChangeBoundsRequest request, Node execShape, Rectangle newBounds) { TransactionalEditingDomain editingDomain = ((IGraphicalEditPart)getHost()).getEditingDomain(); ICommand result = new SetBoundsCommand(editingDomain, DiagramUIMessages.SetLocationCommand_Label_Resize, new EObjectAdapter(execShape), newBounds); diff --git a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/locators/OnLineBorderItemLocator.java b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/locators/OnLineBorderItemLocator.java index 449f3949..aa15dba7 100644 --- a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/locators/OnLineBorderItemLocator.java +++ b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/locators/OnLineBorderItemLocator.java @@ -50,8 +50,8 @@ public void setConstraint(Rectangle constraint) { @Override public Rectangle getValidLocation(Rectangle proposedLocation, IFigure borderItem) { Rectangle body = headerBodyFigure.getBounds().getCopy(); - Point location = new Point(body.x - getConstraint().width / 2, body.y + getConstraint().y); - Rectangle newBounds = getConstraint().getCopy(); + Point location = new Point(body.x - proposedLocation.width / 2, body.y + proposedLocation.y); + Rectangle newBounds = proposedLocation.getCopy(); newBounds.setLocation(location); return newBounds; } diff --git a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/tools/SequenceDragTracker.java b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/tools/SequenceDragTracker.java new file mode 100644 index 00000000..b1ebab7b --- /dev/null +++ b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/tools/SequenceDragTracker.java @@ -0,0 +1,78 @@ +/***************************************************************************** + * Copyright (c) 2018 Christian W. Damus 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: + * Christian W. Damus - Initial API and implementation + *****************************************************************************/ + +package org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.tools; + +import static org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.tools.PrivateToolUtils.getAllowSemanticReorderingModifier; + +import org.eclipse.gef.EditPart; +import org.eclipse.gef.commands.Command; +import org.eclipse.papyrus.infra.gmfdiag.common.snap.PapyrusDragEditPartsTrackerEx; +import org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.edit.parts.LifelineBodyEditPart; +import org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.edit.policies.PrivateRequestUtils; + +/** + * A drag tracker for the sequence diagram that injects custom keyboard modifier information into the requests + * that it produces. + */ +public class SequenceDragTracker extends PapyrusDragEditPartsTrackerEx { + + /** + * Initializes me. + * + * @param sourceEditPart + */ + public SequenceDragTracker(EditPart sourceEditPart) { + super(sourceEditPart); + } + + @Override + protected void updateTargetRequest() { + super.updateTargetRequest(); + + // All requests of interest for re-ordering have location + + PrivateRequestUtils.setAllowSemanticReordering(getTargetRequest(), + getCurrentInput().isModKeyDown(getAllowSemanticReorderingModifier())); + } + + @Override + protected Command getCommand() { + if (getTargetEditPart() == null) { + return null; + } + + return super.getCommand(); + } + + protected EditPart getLifelineBodyEditPart(EditPart editPart) { + EditPart result = null; + + for (EditPart next = editPart; (result == null) && (next != null); next = next.getParent()) { + if (next instanceof LifelineBodyEditPart) { + result = next; + } + } + + return result; + } + + /** + * We don't do clone in this diagram (and {@code Ctrl} is used for semantic re-ordering override on Linux + * platform). + */ + @Override + protected void setCloneActive(boolean cloneActive) { + super.setCloneActive(false); + } + +} diff --git a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/DeferredPaddingCommand.java b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/DeferredPaddingCommand.java index 8a3ed87e..c1037fed 100644 --- a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/DeferredPaddingCommand.java +++ b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/DeferredPaddingCommand.java @@ -12,9 +12,11 @@ package org.eclipse.papyrus.uml.interaction.internal.model.commands; +import static org.eclipse.papyrus.uml.interaction.model.util.LogicalModelPredicates.above; import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.as; import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.mapToInt; +import java.util.Comparator; import java.util.OptionalInt; import org.eclipse.emf.common.command.Command; @@ -31,6 +33,7 @@ import org.eclipse.papyrus.uml.interaction.model.spi.LayoutConstraints; import org.eclipse.papyrus.uml.interaction.model.spi.LayoutConstraints.RelativePosition; import org.eclipse.papyrus.uml.interaction.model.spi.LayoutHelper; +import org.eclipse.papyrus.uml.interaction.model.util.LogicalModelOrdering; import org.eclipse.papyrus.uml.interaction.model.util.SequenceDiagramSwitch; import org.eclipse.uml2.uml.Element; @@ -44,6 +47,8 @@ public class DeferredPaddingCommand extends CommandWrapper { private MElement nudgeElement; + private Comparator> ordering = LogicalModelOrdering.semantically(); + /** * Not instantiable by clients. */ @@ -78,28 +83,36 @@ public static DeferredPaddingCommand get(DependencyContext context, MElement element) { - if (element == null) { + public DeferredPaddingCommand pad(MElement from, MElement to) { + if ((from == null) || (to == null)) { // Have now determined that padding is not required - referenceElement = element; - } else if (referenceElement == null) { - referenceElement = element; - } else if ((referenceElement != element) && referenceElement.precedes(element)) { - referenceElement = element; + referenceElement = null; + nudgeElement = null; + } else if ((referenceElement == null) || before(referenceElement, from)) { + referenceElement = from; + nudgeElement = to; + } else if ((nudgeElement == null) || before(to, nudgeElement)) { + // Nudge the closer element + nudgeElement = to; } return this; } - public DeferredPaddingCommand nudge(MElement element) { - if (element == null) { - // Have now determined that padding is not required - nudgeElement = element; - } else if (nudgeElement == null) { - nudgeElement = element; - } else if ((nudgeElement != element) && element.precedes(nudgeElement)) { - nudgeElement = element; + private boolean before(MElement a, MElement b) { + return (a.getElement() != b.getElement()) && (a.precedes(b) || (ordering.compare(a, b) < 0)); + } + + @Override + protected boolean prepare() { + // Don't create the command, yet. We can always do something + return true; + } + + @Override + public void execute() { + if (super.prepare()) { + super.execute(); } - return this; } @Override @@ -126,7 +139,7 @@ protected int computeNudgeAmount() { MElement padFrom = getPaddableElement(referenceElement); MElement padElement = getPaddableElement(nudgeElement); - if (padFrom.getElement() == padElement.getElement()) { + if (padFrom.getElement() == padElement.getElement() || above(padFrom).test(padElement)) { // This would happen, for example, when re-targeting a message bringins along an // execution specification that emits a message terminating on the re-targeted // message's new receiving lifeline diff --git a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingVerticalExtentData.java b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingVerticalExtentData.java index a647cb26..c6fe8e00 100644 --- a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingVerticalExtentData.java +++ b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingVerticalExtentData.java @@ -74,6 +74,18 @@ public OptionalInt getPendingBottom() { return bottom.isPresent() ? bottom : element.getBottom(); } + /** + * Queries whether an {@code element} is being moved. + * + * @param element + * an element + * @return {@code true} if it is being moved; {@code false}, otherwise, for example if it is being + * reshaped (only top or bottom being moved but not both) or not being changed at all + */ + public boolean isMoving() { + return top.isPresent() && bottom.isPresent(); + } + /** * Query the pending top of an {@code element}. * @@ -119,6 +131,20 @@ static void setPendingVerticalExtent(MElement element, Option } } + /** + * Queries whether an {@code element} is {@linkplain #isMoving() being moved}. + * + * @param element + * an element + * @return {@code true} if it is being moved; {@code false}, otherwise, for example if it is being + * reshaped (only top or bottom being moved but not both) or not being changed at all + * @see #isMoving() + */ + public static boolean isMoving(MElement element) { + return DependencyContext.get().get(element, PendingVerticalExtentData.class) + .map(PendingVerticalExtentData::isMoving).orElse(Boolean.FALSE).booleanValue(); + } + /** * Obtain a predicate that tests whether an element currently spans or will span a given absolute Y * position. diff --git a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/SetCoveredCommand.java b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/SetCoveredCommand.java index 88537003..9eaf446c 100644 --- a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/SetCoveredCommand.java +++ b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/SetCoveredCommand.java @@ -19,6 +19,8 @@ import static org.eclipse.papyrus.uml.interaction.model.util.LogicalModelPredicates.below; import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.as; import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.flatMapToInt; +import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.lessThan; +import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.map; import java.util.ArrayList; import java.util.List; @@ -67,6 +69,8 @@ public class SetCoveredCommand extends ModelCommandWithDependencies> nextOnLifeline; @@ -75,24 +79,24 @@ public class SetCoveredCommand extends ModelCommandWithDependencies end, MLifeline lifeline, + public SetCoveredCommand(MOccurrenceImpl occurrence, MLifeline lifeline, OptionalInt yPosition) { - this(end, lifeline, yPosition, true); + this(occurrence, lifeline, yPosition, true); } - protected SetCoveredCommand(MOccurrenceImpl end, MLifeline lifeline, + protected SetCoveredCommand(MOccurrenceImpl occurrence, MLifeline lifeline, OptionalInt yPosition, boolean handleOppositeSendOrReplyOfExecution) { - super(end); + super(occurrence); this.lifeline = lifeline; this.yPosition = yPosition; - nextOnLifeline = Lifelines.elementAfterAbsolute(lifeline, - yPosition.orElseGet(() -> end.getTop().orElse(0))); - + this.yOriginal = occurrence.getTop(); this.handleOppositeSendOrReply = handleOppositeSendOrReplyOfExecution; + this.nextOnLifeline = Lifelines.elementAfterAbsolute(lifeline, + yPosition.orElseGet(() -> occurrence.getTop().orElse(0))); } protected boolean isChangingLifeline() { @@ -168,9 +172,7 @@ protected Command doCreateCommand() { } } - if (isChangingLifeline()) { - ensurePadding(); - } + ensurePadding(); return result; } @@ -275,13 +277,40 @@ private void collectMessageDependencies(MMessageEnd end, boolean isDisconnecting } // else don't know what to do with it } - // Handle the opposite end if the message is of a synchronous (strictly horizontal) sort + // Handle the opposite end if + // - we're moving an execution that we're attached to + // - the message is of a synchronous (strictly horizontal) sort + // - it would slope backwards Optional other = end.getOtherEnd(); - if (yPosition.isPresent() && other.isPresent() && message.isSynchronous()) { - MMessageEnd opposite = other.get(); - Optional otherCovered = opposite.getCovered(); - // Track this end - otherCovered.map(ll -> opposite.setCovered(ll, yPosition)).ifPresent(commandSink); + OptionalInt otherY = flatMapToInt(other, MElement::getTop); + if (yPosition.isPresent() && otherY.isPresent() && yOriginal.isPresent()) { + MMessageEnd otherEnd = other.get(); + boolean isMovingExec = end.getExecution().filter(exec -> PendingVerticalExtentData.isMoving(exec)) + .isPresent(); + + if (isMovingExec) { + // Does the other end start or finish execution? Move it + int deltaY = yPosition.getAsInt() - yOriginal.getAsInt(); + + Optional execToMove = (otherEnd.isStart() || otherEnd.isFinish()) + ? otherEnd.getExecution() + : Optional.empty(); + if (execToMove.isPresent()) { + execToMove.map(exec -> exec.setOwner(exec.getOwner(), map(exec.getTop(), t -> t + deltaY), + map(exec.getBottom(), b -> b + deltaY))).ifPresent(commandSink); + } else { + Optional otherCovered = other.flatMap(MMessageEnd::getCovered); + OptionalInt otherNewY = map(otherY, y -> y + deltaY); + + // Track this end + otherCovered.map(ll -> otherEnd.setCovered(ll, otherNewY)).ifPresent(commandSink); + } + } else if (message.isSynchronous() + || (otherEnd.isReceive() && lessThan(otherEnd.getTop(), yPosition))) { + Optional otherCovered = otherEnd.getCovered(); + // Track this end + otherCovered.map(ll -> otherEnd.setCovered(ll, yPosition)).ifPresent(commandSink); + } } } @@ -526,16 +555,13 @@ protected boolean validateCreation(int y) { } protected void ensurePadding() { - MElement element = getTarget(); // From which element do we need to ensure padding? - MElement padFrom = element instanceof MMessageEnd - ? ((MMessageEnd)element).getOwner() - : element; + MElement padFrom = getTarget(); // Do we have an element that needs padding before it? MElement nudge = nextOnLifeline.orElse(null); - DeferredPaddingCommand.get(element).padFrom(padFrom).nudge(nudge); + DeferredPaddingCommand.get(padFrom).pad(padFrom, nudge); } /** diff --git a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/SetOwnerCommand.java b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/SetOwnerCommand.java index 7711836f..97cabbb5 100644 --- a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/SetOwnerCommand.java +++ b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/SetOwnerCommand.java @@ -16,11 +16,11 @@ import static org.eclipse.papyrus.uml.interaction.internal.model.commands.PendingVerticalExtentData.affectedOccurrences; import static org.eclipse.papyrus.uml.interaction.model.util.Executions.getLifelineView; import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.as; +import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.map; -import java.util.Collections; -import java.util.List; import java.util.Optional; import java.util.OptionalInt; +import java.util.function.UnaryOperator; import org.eclipse.emf.common.command.Command; import org.eclipse.emf.common.command.IdentityCommand; @@ -32,7 +32,6 @@ import org.eclipse.papyrus.uml.interaction.model.MElement; import org.eclipse.papyrus.uml.interaction.model.MExecution; import org.eclipse.papyrus.uml.interaction.model.MLifeline; -import org.eclipse.papyrus.uml.interaction.model.MMessageEnd; import org.eclipse.papyrus.uml.interaction.model.MOccurrence; import org.eclipse.papyrus.uml.interaction.model.util.Lifelines; import org.eclipse.papyrus.uml.interaction.model.util.SequenceDiagramSwitch; @@ -54,9 +53,6 @@ public class SetOwnerCommand extends ModelCommandWithDependencies> nextOnLifeline; - // If the element is an execution, store its spanned occurrences for later - private final List> spannedOccurrences; - /** * Initializes me. * @@ -73,8 +69,6 @@ public SetOwnerCommand(MElementImpl element, MElement Lifelines .elementAfterAbsolute(lifeline, top.orElseGet(() -> element.getTop().orElse(0)))); - spannedOccurrences = as(Optional.of(element), MExecution.class).map(MExecution::getOccurrences) - .orElse(Collections.emptyList()); // Publish this ownership change in the dependency context for other commands to find PendingChildData.setPendingChild(newOwner, element); @@ -110,9 +104,7 @@ protected Command doCreateCommand() { public Command caseMExecution(MExecution execution) { Command result = createCommand(execution, (MLifeline)newOwner); - if (isChangingOwner()) { - ensurePadding(); - } + ensurePadding(); return result; } @@ -171,29 +163,40 @@ protected Command createCommand(MExecution execution, MLifeline lifeline) { * @return the dependencies command */ protected Optional dependencies(MExecution execution, MLifeline lifeline) { - // Occurrences spanned by the execution, including its start and finish. They move - // according to the execution, maintaining their relative position. Note that - // nested executions will be handled implicitly by either their start or finish. + // Are we moving the execution or one of its start/finish occurrences? We're moving an + // occurrence if one of them already has a SetCoveredCommand + Optional> start = execution.getStart(); + Optional> finish = execution.getFinish(); + boolean movingExecution = !(start.isPresent() && hasDependency(start.get(), SetCoveredCommand.class)) + && !(finish.isPresent() && hasDependency(finish.get(), SetCoveredCommand.class)); + // Compute not only currently spanned occurrences that will need updating, but // also future spanned occurrences - - return affectedOccurrences(execution).map(occ -> { - OptionalInt where = occ.getTop(); - return defer(() -> occ.setCovered(lifeline, where)); - }).reduce(chaining()); + if (movingExecution) { + int deltaTop = map(this.top, t -> t - execution.getTop().getAsInt()).orElse(0); + UnaryOperator topMapping = deltaTop == 0 ? UnaryOperator.identity() + : top_ -> map(top_, t -> t + deltaTop); + return affectedOccurrences(execution) + .map(occ -> occ.setCovered(lifeline, topMapping.apply(occ.getTop()))).reduce(chaining()); + } else { + // Occurrences spanned by the execution, including its start and finish. They move + // according to the execution, maintaining their relative position. Note that + // nested executions will be handled implicitly by either their start or finish. + return affectedOccurrences(execution).map(occ -> { + OptionalInt where = occ.getTop(); + return defer(() -> occ.setCovered(lifeline, where)); + }).reduce(chaining()); + } } protected void ensurePadding() { - MElement element = getTarget(); // From which element do we need to ensure padding? - MElement padFrom = element instanceof MMessageEnd - ? ((MMessageEnd)element).getOwner() - : element; + MElement padFrom = getTarget(); // Do we have an element that needs padding before it? MElement nudge = nextOnLifeline.orElse(null); - DeferredPaddingCommand.get(element).padFrom(padFrom).nudge(nudge); + DeferredPaddingCommand.get(padFrom).pad(padFrom, nudge); } } diff --git a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/model/util/Optionals.java b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/model/util/Optionals.java index 84dc6f47..8374baec 100644 --- a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/model/util/Optionals.java +++ b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/model/util/Optionals.java @@ -16,6 +16,7 @@ import java.util.OptionalInt; import java.util.function.Function; import java.util.function.IntFunction; +import java.util.function.IntPredicate; import java.util.function.IntUnaryOperator; import java.util.function.Supplier; import java.util.function.ToIntFunction; @@ -222,4 +223,31 @@ public static OptionalInt max(OptionalInt a, OptionalInt b) { public static Stream stream(Optional... optionals) { return Stream.of(optionals).filter(Optional::isPresent).map(Optional::get); } + + /** + * Is an optional integer less than another? + * + * @param a + * an optional integer + * @param b + * another optional integer + * @return {@code true} if both values are present and {@code a} is less than {@code b}; {@code false} + * under any other circumstance + */ + public static boolean lessThan(OptionalInt a, OptionalInt b) { + return a.orElse(Integer.MAX_VALUE) < b.orElse(Integer.MIN_VALUE); + } + + /** + * Filter an optional integer {@code value}. + * + * @param value + * an optional integer value + * @param predicate + * an integer predicate + * @return the {@code value} if it satisfies the {@code predicate}, otherwise empty + */ + public static OptionalInt filter(OptionalInt value, IntPredicate predicate) { + return (value.isPresent() && predicate.test(value.getAsInt())) ? value : OptionalInt.empty(); + } } diff --git a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/ExecutionSpecificationDragEditPolicyUITest.java b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/ExecutionSpecificationDragEditPolicyUITest.java new file mode 100644 index 00000000..b3a6e452 --- /dev/null +++ b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/ExecutionSpecificationDragEditPolicyUITest.java @@ -0,0 +1,335 @@ +/***************************************************************************** + * Copyright (c) 2018 Christian W. Damus 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: + * Christian W. Damus - Initial API and implementation + *****************************************************************************/ + +package org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.edit.policies.tests; + +import static org.eclipse.papyrus.infra.gmfdiag.common.utils.DiagramEditPartsUtil.getChildByEObject; +import static org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.matchers.GEFMatchers.is; +import static org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.matchers.GEFMatchers.isPoint; +import static org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.matchers.GEFMatchers.isRect; +import static org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.matchers.GEFMatchers.EditParts.isAt; +import static org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.matchers.GEFMatchers.EditParts.isBounded; +import static org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.matchers.GEFMatchers.EditParts.runs; +import static org.eclipse.papyrus.uml.interaction.tests.matchers.NumberMatchers.gt; +import static org.eclipse.papyrus.uml.interaction.tests.matchers.NumberMatchers.gte; +import static org.hamcrest.CoreMatchers.anything; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.eclipse.draw2d.geometry.Point; +import org.eclipse.draw2d.geometry.PointList; +import org.eclipse.draw2d.geometry.Rectangle; +import org.eclipse.gef.EditPart; +import org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.matchers.GEFMatchers; +import org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.rules.AutoFixture; +import org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.rules.AutoFixtureRule; +import org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.rules.Maximized; +import org.eclipse.papyrus.uml.interaction.tests.rules.ModelResource; +import org.eclipse.uml2.uml.ExecutionSpecification; +import org.eclipse.uml2.uml.Lifeline; +import org.eclipse.uml2.uml.Message; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +/** + * Test cases for the {@code ExecutionSpecificationDragEditPolicy} class. + */ +@RunWith(Enclosed.class) +public class ExecutionSpecificationDragEditPolicyUITest { + + @ClassRule + public static TestRule TOLERANCE = GEFMatchers.defaultTolerance(1); + + /** + * Initializes me. + */ + public ExecutionSpecificationDragEditPolicyUITest() { + super(); + } + + // + // Nested test suites + // + + /** + * Basic move/resize drag-and-drop use cases. + */ + @ModelResource("kitchen-sink.di") + @Maximized + public static class Basic extends AbstractGraphicalEditPolicyUITest { + @Rule + public final AutoFixtureRule autoFixtures = new AutoFixtureRule(this); + + @AutoFixture("exec1") + private ExecutionSpecification exec; + + @AutoFixture + private EditPart execEP; + + @AutoFixture("m1") + private Message request; + + @AutoFixture + private EditPart requestEP; + + @AutoFixture("m2") + private Message reply; + + @AutoFixture + private EditPart replyEP; + + @AutoFixture + private ExecutionSpecification exec2; + + @AutoFixture + private EditPart exec2EP; + + @AutoFixture("L1") + private Lifeline l1; + + @AutoFixture + private EditPart l1EP; + + @AutoFixture("L2") + private Lifeline l2; + + @AutoFixture + private EditPart l2EP; + + @AutoFixture + private Message m3; + + @AutoFixture + private EditPart m3EP; + + /** + * Initializes me. + */ + public Basic() { + super(); + } + + @Test + public void moveExecutionUp() { + moveExecution(-50); + } + + void moveExecution(int delta) { + Point requestSend = getSource(requestEP); + Point requestRecv = getTarget(requestEP); + Point replySend = getSource(replyEP); + Point replyRecv = getTarget(replyEP); + Rectangle execBounds = getBounds(execEP); + + // First, select the execution to activate selection handles + Point grabAt = getCenter(execEP); + editor.select(grabAt); + + Point dropAt = new Point(grabAt.x(), grabAt.y() + delta); + editor.drag(grabAt, dropAt); + + execBounds.translate(0, delta); + requestSend.translate(0, delta); + requestRecv.translate(0, delta); + replySend.translate(0, delta); + replyRecv.translate(0, delta); + + assertThat("Execution not moved", execEP, isBounded(isRect(execBounds))); + assertThat("Request message not moved", requestEP, + runs(isPoint(requestSend), isPoint(requestRecv))); + assertThat("Reply message not moved", replyEP, runs(isPoint(replySend), isPoint(replyRecv))); + } + + @Test + public void moveExecutionDown() { + // Move the execution close enough to the next one below that it must be nudged + moveExecution(35); + + Rectangle execBounds = getBounds(execEP); + + int padding = 10; + assertThat("Next execution not nudged", exec2EP, + isBounded(anything(), is(execBounds.bottom() + padding), anything(), anything())); + } + + /** + * Verify that a move that would require semantic re-ordering is blocked without the keyboard + * modifier. + */ + @Test + public void attemptMoveWithSemanticReordering() { + int delta = 60; + Rectangle execBounds = getBounds(execEP); + + // First, select the execution to activate selection handles + Point grabAt = getCenter(execEP); + editor.select(grabAt); + + Point dropAt = new Point(grabAt.x(), grabAt.y() + delta); + editor.drag(grabAt, dropAt); + + assertThat("Execution moved", execEP, isBounded(isRect(execBounds))); + } + + /** + * Verify that a move that requires semantic re-ordering is allowed with the keyboard modifier. + */ + @Test + public void moveWithSemanticReordering() { + int delta = 60; + Rectangle execBounds = getBounds(execEP); + + // First, select the execution to activate selection handles + Point grabAt = getCenter(execEP); + editor.select(grabAt); + + Point dropAt = new Point(grabAt.x(), grabAt.y() + delta); + editor.with(editor.allowSemanticReordering(), () -> editor.drag(grabAt, dropAt)); + + execBounds.translate(0, delta); + assertThat("Execution not moved", execEP, isBounded(isRect(execBounds))); + } + + /** + * Verify that a change of lifeline is blocked without the keyboard modifier. + */ + @Test + public void attemptChangeLifeline() { + int l1X = getBounds(l1EP).getCenter().x(); + int l2X = getBounds(l2EP).getCenter().x(); + int exec2Top = getTop(exec2EP); + + // First, select the execution to activate selection handles + Point grabAt = getCenter(exec2EP); + editor.select(grabAt); + + Point dropAt = new Point(l2X, grabAt.y()); + editor.drag(grabAt, dropAt); + // Drag-and-drop creates a new edit-part + exec2EP = getChildByEObject(exec2, editor.getDiagramEditPart(), false); + + assertThat("Execution was moved", exec2EP, isAt(l1X - (EXEC_WIDTH / 2), exec2Top)); + } + + /** + * Verify drag and drop of execution specification to another lifeline. + */ + @Test + public void changeLifeline() { + int l2X = getBounds(l2EP).getCenter().x(); + int exec2Top = getTop(exec2EP); + + // First, select the execution to activate selection handles + Point grabAt = getCenter(exec2EP); + editor.select(grabAt); + + Point dropAt = new Point(l2X, grabAt.y()); + editor.with(editor.allowSemanticReordering(), () -> editor.drag(grabAt, dropAt)); + // Drag-and-drop creates a new edit-part + exec2EP = getChildByEObject(exec2, editor.getDiagramEditPart(), false); + + assertThat("Execution not moved", exec2EP, isAt(l2X - (EXEC_WIDTH / 2), exec2Top)); + + PointList m3Points = getPoints(m3EP); + assertThat("Not a self-message", m3Points.size(), gt(2)); + + Point m3Send = m3Points.getFirstPoint(); + Point m3Recv = m3Points.getLastPoint(); + assertThat(m3Send, isPoint(is(l2X + (EXEC_WIDTH / 2)), anything())); + assertThat(m3Recv, isPoint(is(l2X + (EXEC_WIDTH / 2)), gte(m3Send.y() + 20))); + } + + } + + /** + * Basic move/resize drag-and-drop use cases. + */ + @ModelResource("65-moving.di") + @Maximized + public static class WithMessagesAttached extends AbstractGraphicalEditPolicyUITest { + @Rule + public final AutoFixtureRule autoFixtures = new AutoFixtureRule(this); + + @AutoFixture("exec1") + private ExecutionSpecification exec; + + @AutoFixture + private EditPart execEP; + + @AutoFixture + private Message async1; + + @AutoFixture + private EditPart async1EP; + + @AutoFixture + private Message async2; + + @AutoFixture + private EditPart async2EP; + + @AutoFixture + private Message async3; + + @AutoFixture + private EditPart async3EP; + + @AutoFixture + private ExecutionSpecification exec2; + + @AutoFixture + private EditPart exec2EP; + + /** + * Initializes me. + */ + public WithMessagesAttached() { + super(); + } + + @Test + public void moveExecutionDown() { + int delta = 15; + + PointList async1Geom = getPoints(async1EP); + PointList async2Geom = getPoints(async2EP); + PointList async3Geom = getPoints(async3EP); + Rectangle exec2Geom = getBounds(exec2EP); + + // First, select the execution to activate selection handles + Point grabAt = getCenter(execEP); + editor.select(grabAt); + + Point dropAt = new Point(grabAt.x(), grabAt.y() + delta); + editor.drag(grabAt, dropAt); + + async1Geom.translate(0, delta); + async2Geom.translate(0, delta); + async3Geom.translate(0, delta); + exec2Geom.translate(0, delta); + + assertThat("Async message 1 was changed", async1EP, + runs(isPoint(async1Geom.getFirstPoint()), isPoint(async1Geom.getLastPoint()))); + assertThat("Async message 2 was changed", async2EP, + runs(isPoint(async2Geom.getFirstPoint()), isPoint(async2Geom.getLastPoint()))); + assertThat("Async message 3 was changed", async3EP, + runs(isPoint(async3Geom.getFirstPoint()), isPoint(async3Geom.getLastPoint()))); + assertThat("Execution started by sunc message 2 not moved", exec2EP, + isAt(isPoint(exec2Geom.getLocation()))); + } + + } + +} diff --git a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/parsers/tests/MessageParserTest_PTest.java b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/parsers/tests/MessageParserTest_PTest.java index abccd175..9d1546c8 100644 --- a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/parsers/tests/MessageParserTest_PTest.java +++ b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/parsers/tests/MessageParserTest_PTest.java @@ -30,7 +30,7 @@ import org.eclipse.gmf.runtime.emf.ui.services.parser.ISemanticParser; import org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.parsers.MessageParser; import org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.rules.ModelFixture; -import org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.rules.ModelResource; +import org.eclipse.papyrus.uml.interaction.tests.rules.ModelResource; import org.eclipse.uml2.uml.Message; import org.hamcrest.FeatureMatcher; import org.hamcrest.Matcher; @@ -38,8 +38,7 @@ import org.junit.Test; /** - * Tests for the {@link MessageParser} class. The parser needs a workspace for - * Papyrus's i18n support. + * Tests for the {@link MessageParser} class. The parser needs a workspace for Papyrus's i18n support. * * @author Christian W. Damus */ @@ -81,8 +80,8 @@ public void requestNamedParameters() { @Test public void replyWithAssignedAndUnassignedOutputs() { Message reply = model.getMessage("whatsIt", "thing"); - assertThat(reply, rendersAs( - "thing::ok=getStuff(thing::content=text: \"Hello, world\", expiration: 60): true")); + assertThat(reply, + rendersAs("thing::ok=getStuff(thing::content=text: \"Hello, world\", expiration: 60): true")); } @Test @@ -103,7 +102,7 @@ public void semanticElementsBeingParsed() { Message reply = model.getMessage("whatsIt", "thing"); @SuppressWarnings("unchecked") - Set set = new HashSet<>(((ISemanticParser) parser).getSemanticElementsBeingParsed(reply)); + Set set = new HashSet<>(((ISemanticParser)parser).getSemanticElementsBeingParsed(reply)); Iterator iter = EcoreUtil.getAllContents(Collections.singleton(reply)); iter.forEachRemaining(next -> assertThat(set, hasItem(next))); diff --git a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/ModelResource.java b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/AutoFixture.java similarity index 68% rename from tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/ModelResource.java rename to tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/AutoFixture.java index a6ca8b5c..25bcf02a 100644 --- a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/ModelResource.java +++ b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/AutoFixture.java @@ -1,6 +1,6 @@ /***************************************************************************** * Copyright (c) 2018 Christian W. Damus 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 @@ -12,21 +12,19 @@ package org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.rules; -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Retention; import java.lang.annotation.Target; /** - * This is the {@code ModelResource} type. Enjoy. - * - * @author Christian W. Damus + * Annotation for fields that represent fixtures in the contextual model or editor, usually an + * {@link EditorFixture}. */ @Retention(RUNTIME) -@Target({ TYPE, METHOD }) -public @interface ModelResource { - /** The paths relative to the test class of the model resources to load. */ - String[] value(); +@Target(FIELD) +public @interface AutoFixture { + /** An optional element name to search for, which may be qualified or not. */ + String value() default ""; } diff --git a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/AutoFixtureRule.java b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/AutoFixtureRule.java new file mode 100644 index 00000000..ca9ff81a --- /dev/null +++ b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/AutoFixtureRule.java @@ -0,0 +1,381 @@ +/***************************************************************************** + * Copyright (c) 2018 Christian W. Damus 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: + * Christian W. Damus - Initial API and implementation + *****************************************************************************/ + +package org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.rules; + +import static java.util.Collections.singleton; +import static org.eclipse.uml2.common.util.UML2Util.getValidJavaIdentifier; +import static org.junit.Assert.fail; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.emf.common.util.TreeIterator; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.gef.EditPart; +import org.eclipse.papyrus.infra.gmfdiag.common.utils.DiagramEditPartsUtil; +import org.eclipse.papyrus.uml.interaction.tests.rules.ModelFixture; +import org.eclipse.uml2.uml.ActivityEdge; +import org.eclipse.uml2.uml.AssociationClass; +import org.eclipse.uml2.uml.Message; +import org.eclipse.uml2.uml.NamedElement; +import org.eclipse.uml2.uml.Namespace; +import org.eclipse.uml2.uml.Relationship; +import org.eclipse.uml2.uml.Transition; +import org.eclipse.uml2.uml.util.UMLSwitch; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * A test rule that automatically populates fields annotated with {@link AutoFixture @AutoFixture} from the + * test rule that is some kind of {@link ModelFixture}. + */ +public class AutoFixtureRule implements TestRule { + + private static final Pattern EDIT_PART_FIXTURE = Pattern.compile(".*(?=EP|EditPart)"); + + private final Object test; + + private BiFunction, Object> resolver; + + /** + * Initializes me. + */ + public AutoFixtureRule(Object testOrTestClass) { + super(); + + this.test = testOrTestClass; + + List, Object>> resolvers = Arrays.asList( + this::resolveQualifiedName, // + this::resolveSimpleName, // + this::resolveEditPart); + resolver = (model, spec) -> resolvers.stream().map(r -> r.apply(model, spec)).filter(Objects::nonNull) + .findFirst().orElse(null); + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + + @Override + public void evaluate() throws Throwable { + ModelFixture model = getField(ModelFixture.class).get(); + + getAutoFixtures().forEach(fixture -> { + fixture.set(resolver.apply(model, fixture)); + }); + + try { + base.evaluate(); + } finally { + getAutoFixtures().forEach(FieldAccess::clear); + } + } + }; + } + + private Stream getFields() { + return getFields(test); + } + + private static Stream getFields(Object owner) { + boolean isStatic = owner instanceof Class; + return isStatic ? getFields((Class)owner, isStatic) : getFields(owner.getClass(), isStatic); + } + + private static Stream getFields(Class owner, boolean isStatic) { + Stream result = Stream.of(owner.getDeclaredFields()) + .filter(f -> Modifier.isStatic(f.getModifiers()) == isStatic); + + Class superClass = owner.getSuperclass(); + if ((superClass != null) && superClass != Object.class) { + result = Stream.concat(result, getFields(superClass, isStatic)); + } + + return result; + } + + private FieldAccess getField(Class type) { + return new FieldAccess<>(type, test); + } + + private FieldAccess getField(String name, Class type) { + return new FieldAccess<>(name, type, test); + } + + private Stream> getAutoFixtures() { + List list = getFields().collect(Collectors.toList()); + return getFields().filter(f -> f.isAnnotationPresent(AutoFixture.class)) + .map(f -> new FieldAccess<>(f, Object.class, test)); + } + + // + // Resolvers + // + + private Object resolveQualifiedName(ModelFixture model, FieldAccess fixture) { + Object result = null; + + String[] parts = fixture.getFixtureName().split("::"); + if ((parts.length > 0) && parts[0].equals(model.getModel().getName())) { + NamedElement next = model.getModel(); + + for (int i = 1; (next != null) && (i < parts.length); i++) { + if (!(next instanceof Namespace)) { + fail("Not a namespace: " + next); + } + + next = ((Namespace)next).getMember(parts[i]); + } + + result = next; + } + + return fixture.asAssignable(result); + } + + private Object resolveSimpleName(ModelFixture model, FieldAccess fixture) { + Object result = null; + + String name = fixture.getFixtureName(); + if (!name.contains("::")) { + for (TreeIterator iter = EcoreUtil + .getAllContents(singleton(model.getModel())); (result == null) && iter.hasNext();) { + + EObject next = iter.next(); + if (next instanceof NamedElement) { + if (name.equals(getValidJavaIdentifier(((NamedElement)next).getName())) + && fixture.isAssignable(next)) { + + result = next; + } + } else { + iter.prune(); + } + } + } + + return result; + } + + private Object resolveEditPart(ModelFixture model, FieldAccess fixture) { + Object result = null; + + if ((model instanceof EditorFixture) && fixture.isAssignable(EditPart.class)) { + EditorFixture editor = (EditorFixture)model; + + FieldAccess element = getField(fixture.getFixtureName(EDIT_PART_FIXTURE), + EObject.class); + if (element != null) { + EObject fixtureElement = element.get(); + result = DiagramEditPartsUtil.getChildByEObject(fixtureElement, editor.getDiagramEditPart(), + isEdge(fixtureElement)); + } + } + + return result; + } + + @SuppressWarnings("hiding") + static boolean isEdge(EObject object) { + return new UMLSwitch() { + @Override + public Boolean caseMessage(Message object) { + return true; + } + + @Override + public Boolean caseAssociationClass(AssociationClass object) { + return false; + } + + @Override + public Boolean caseRelationship(Relationship object) { + return true; + } + + @Override + public Boolean caseActivityEdge(ActivityEdge object) { + return true; + } + + @Override + public Boolean caseTransition(Transition object) { + return true; + } + + @Override + public Boolean defaultCase(EObject object) { + return false; + } + }.doSwitch(object); + } + + // + // Nested types + // + + private static final class FieldAccess { + private final Field field; + + private final Object owner; + + private final Class type; + + FieldAccess(Class type, Object owner) { + super(); + + try { + this.field = getFields(owner).filter(f -> type.isAssignableFrom(f.getType())).findAny().get(); + setAccessible(); + + this.owner = owner; + this.type = field.getType().asSubclass(type); + } catch (Exception e) { + e.printStackTrace(); + fail(String.format("Cannnot access fixture of type %s", type.getName())); + throw new Error(); // Unreachable + } + } + + FieldAccess(String name, Class type, Object owner) { + super(); + + try { + this.field = getFields(owner).filter(f -> name.equals(f.getName())).findFirst().get(); + setAccessible(); + + this.owner = owner; + this.type = field.getType().asSubclass(type); + } catch (Exception e) { + e.printStackTrace(); + fail(String.format("Cannnot access fixture %s", name)); + throw new Error(); // Unreachable + } + } + + FieldAccess(Field field, Class type, Object owner) { + super(); + + try { + this.field = field; + setAccessible(); + + this.owner = owner; + this.type = field.getType().asSubclass(type); + } catch (Exception e) { + e.printStackTrace(); + fail(String.format("Cannnot access fixture %s", field.getName())); + throw new Error(); // Unreachable + } + } + + private void setAccessible() { + if (!Modifier.isPublic(field.getModifiers())) { + field.setAccessible(true); + } + } + + String getName() { + return field.getName(); + } + + String getFixtureName() { + AutoFixture auto = getAutoFixture(); + String result = (auto == null) ? "" : auto.value(); + + return result.isEmpty() ? getName() : result; + } + + String getFixtureName(Pattern regex) { + String result = getFixtureName(); + Matcher m = regex.matcher(result); + if (m.find()) { + result = m.group(); + } + return result; + } + + AutoFixture getAutoFixture() { + return field.getAnnotation(AutoFixture.class); + } + + boolean isAssignable(Object object) { + Class objectType; + + if (object == null) { + objectType = Void.class; + } else if ((object instanceof Class)) { + objectType = (Class)object; + } else { + objectType = object.getClass(); + } + + return field.getType().isAssignableFrom(objectType); + } + + T asAssignable(Object object) { + return isAssignable(object) ? type.cast(object) : null; + } + + T get() { + try { + return type.cast(field.get(owner)); + } catch (Exception e) { + e.printStackTrace(); + fail(String.format("Could not get fixture %s of type %s", field.getName(), type.getName())); + return null; // Unreachable + } + } + + void set(Object resolved) { + if (resolved == null) { + fail(String.format("Could not resolve fixture %s of type %s", field.getName(), + type.getName())); + return; // Unreachable + } + + if (type.isInstance(resolved)) { + try { + field.set(owner, resolved); + } catch (Exception e) { + e.printStackTrace(); + fail(String.format("Could not set fixture %s of type %s", field.getName(), + type.getName())); + } + } else { + fail(String.format("Could not set a %s into fixture %s of type %s", + resolved.getClass().getName(), field.getName(), type.getName())); + } + } + + void clear() { + try { + field.set(owner, null); + } catch (Exception e) { + e.printStackTrace(); + // Best effort. Just continue with tests + } + } + } +} diff --git a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/ModelFixture.java b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/ModelFixture.java index db51b3fb..f593ca24 100644 --- a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/ModelFixture.java +++ b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/ModelFixture.java @@ -12,88 +12,45 @@ package org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.rules; -import static org.hamcrest.CoreMatchers.anything; -import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.fail; - -import java.net.URL; -import java.util.Collection; import java.util.NoSuchElementException; import java.util.Objects; -import java.util.Optional; import java.util.function.Predicate; -import org.eclipse.emf.common.EMFPlugin; -import org.eclipse.emf.common.util.URI; -import org.eclipse.emf.ecore.EClass; -import org.eclipse.emf.ecore.EStructuralFeature; -import org.eclipse.emf.ecore.resource.Resource; -import org.eclipse.emf.ecore.resource.ResourceSet; -import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; -import org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl; import org.eclipse.gmf.runtime.notation.Diagram; -import org.eclipse.gmf.runtime.notation.NotationPackage; import org.eclipse.papyrus.uml.diagram.sequence.runtime.util.MessageUtil; -import org.eclipse.uml2.common.util.CacheAdapter; -import org.eclipse.uml2.common.util.UML2Util; import org.eclipse.uml2.uml.Interaction; import org.eclipse.uml2.uml.Message; import org.eclipse.uml2.uml.NamedElement; -import org.eclipse.uml2.uml.Package; -import org.eclipse.uml2.uml.UMLPackage; -import org.eclipse.uml2.uml.resources.util.UMLResourcesUtil; -import org.eclipse.uml2.uml.util.UMLUtil; import org.junit.ClassRule; -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; /** * A test fixture that loads an UML model containing an {@link Interaction}. * * @author Christian W. Damus */ -public class ModelFixture implements TestRule { +public class ModelFixture extends org.eclipse.papyrus.uml.interaction.tests.rules.ModelFixture { private static final String SEQUENCE_DIAGRAM_TYPE = "PapyrusUMLSequenceDiagram"; //$NON-NLS-1$ - // private static final String SEQUENCE_DIAGRAM_TYPE = - // "LightweightSequenceDiagram"; //$NON-NLS-1$ - - private final Class testClass; - private final String path; - - private ResourceSet rset; - private Package model; - private Interaction interaction; - private Optional sequenceDiagram; /** * Initializes me. * * @param testClass - * the test class in which context to load the resource. May be - * {@code null} for an ordinary {@code Rule} but required for a - * {@link ClassRule} + * the test class in which context to load the resource. May be {@code null} for an ordinary + * {@code Rule} but required for a {@link ClassRule} * @param path - * the path relative to the {@code testClass} of the UML resource to - * load + * the path relative to the {@code testClass} of the UML resource to load */ public ModelFixture(Class testClass, String path) { - super(); - - this.testClass = testClass; - this.path = path; + super(testClass, path); } /** * Initializes me. * * @param testClass - * the test class in which context to load the resource. May be - * {@code null} for an ordinary {@code Rule} but required for a - * {@link ClassRule} + * the test class in which context to load the resource. May be {@code null} for an ordinary + * {@code Rule} but required for a {@link ClassRule} */ public ModelFixture(Class testClass) { this(testClass, null); @@ -103,8 +60,7 @@ public ModelFixture(Class testClass) { * Initializes me. * * @param path - * the path relative to the {@code testClass} of the UML resource to - * load + * the path relative to the {@code testClass} of the UML resource to load */ public ModelFixture(String path) { this(null, path); @@ -117,178 +73,21 @@ public ModelFixture() { this(null, null); } - public Package getModel() { - return model; - } - - public Interaction getInteraction() { - return interaction; - } - - public Optional getSequenceDiagram() { - return sequenceDiagram; - } - @Override - public Statement apply(Statement base, Description description) { - return new Statement() { - - @Override - public void evaluate() throws Throwable { - starting(description); - - try { - base.evaluate(); - } finally { - finished(description); - } - } - }; - } - - protected void starting(Description description) { - Class context = testClass; - if (context == null) { - context = description.getTestClass(); - if (context == null) { - fail("Explicit test class required for @ClassRule"); - } - } - - final String[] paths = getPaths(description); - for (String path : paths) { - URL resourceURL = context.getResource(path); - if (resourceURL == null) { - fail("Resource not found: " + path); - } - - if (rset == null) { - rset = new ResourceSetImpl(); - if (!EMFPlugin.IS_ECLIPSE_RUNNING) { - standaloneSetup(rset); - } - } - - Package package_ = UML2Util.load(rset, URI.createURI(resourceURL.toExternalForm(), true), - UMLPackage.Literals.PACKAGE); - if (model == null) { - model = package_; - } - } - - assertThat("No UML package found in " + path, model, notNullValue()); - - interaction = (Interaction) UML2Util.findEObject(model.eAllContents(), - UMLPackage.Literals.INTERACTION::isInstance); - assertThat("No UML interaction found in " + path, interaction, notNullValue()); - - // Look for a sequence diagram - sequenceDiagram = CacheAdapter.getCacheAdapter(interaction).getInverseReferences(interaction) - .stream() - .filter(setting -> setting - .getEStructuralFeature() == NotationPackage.Literals.VIEW__ELEMENT) - .map(EStructuralFeature.Setting::getEObject).filter(Diagram.class::isInstance) - .map(Diagram.class::cast).filter(diagram -> SEQUENCE_DIAGRAM_TYPE.equals(diagram.getType())) - .findAny(); - } - - protected void finished(Description description) { - interaction = null; - model = null; - - rset.getResources().forEach(Resource::unload); - rset.getResources().clear(); - rset.eAdapters().clear(); - rset = null; - } - - protected String[] getPaths(Description description) { - String[] result; - - if (path != null) { - result = new String[] { path }; - } else { - ModelResource resource = description.getAnnotation(ModelResource.class); - if ((resource == null) && (description.getTestClass() != null)) { - resource = description.getTestClass().getAnnotation(ModelResource.class); - } - if (resource == null) { - fail("Required @ModelResource annotation is missing for " + description); - } - result = resource.value(); - } - - return result; - } - - /** - * Get the named element. Fails the test if the element is not found. - * - * @param qualifiedName - * the qualified name of an element to get - * @return the element - */ - public NamedElement getElement(String qualifiedName) { - Collection result = UMLUtil.findNamedElements(rset, qualifiedName); - - assertThat("no such element: " + qualifiedName, result, hasItem(anything())); - - return result.iterator().next(); - } - - /** - * Get the named element. Fails the test if the element is not found. - * - * @param qualifiedName - * the qualified name of an element to get - * @param type - * the type of element to retrieve - * - * @return the element - * - * @param - * the element type to retrieve - */ - public T getElement(String qualifiedName, Class type) { - - Collection result = UMLUtil.findNamedElements(rset, qualifiedName, false, - (EClass) UMLPackage.eINSTANCE.getEClassifier(type.getSimpleName())); - - assertThat("no such element: " + qualifiedName, result, hasItem(anything())); - - return type.cast(result.iterator().next()); - } - - /** - * Configures a resource set for stand-alone test execution. - * - * @param rset - * a resource set to configure - */ - public static void standaloneSetup(ResourceSet rset) { - UMLResourcesUtil.init(rset); - - // Initialize the diagram notation - NotationPackage.Literals.DIAGRAM.eClass(); - - rset.getResourceFactoryRegistry().getExtensionToFactoryMap().put("notation", - new XMIResourceFactoryImpl()); + protected boolean isSequenceDiagram(Diagram diagram) { + return SEQUENCE_DIAGRAM_TYPE.equals(diagram.getType()); } /** - * Get the first available message between two lifelines in my - * {@link #getInteraction() interaction}. + * Get the first available message between two lifelines in my {@link #getInteraction() interaction}. * * @param from * the sending lifeline name * @param to * the receiving lifeline name - * * @return the message - * * @throws NoSuchElementException * if no such message exists in the interaction - * * @see #getInteraction() */ public Message getMessage(String from, String to) { @@ -297,24 +96,19 @@ public Message getMessage(String from, String to) { } /** - * Get a specific message between two lifelines in my {@link #getInteraction() - * interaction}. This is a useful alternative to the simple - * {@code #getMessage(String, String)} when there are multiple messages in the - * same direction between two lifelines. + * Get a specific message between two lifelines in my {@link #getInteraction() interaction}. This is a + * useful alternative to the simple {@code #getMessage(String, String)} when there are multiple messages + * in the same direction between two lifelines. * * @param from * the sending lifeline name * @param to * the receiving lifeline name * @param index - * which of the messages between the two lifelines, in sequence - * order, to retrieve - * + * which of the messages between the two lifelines, in sequence order, to retrieve * @return the message - * * @throws NoSuchElementException * if no such message exists in the interaction - * * @see #getInteraction() * @see #getMessage(String, String) */ diff --git a/tests/org.eclipse.papyrus.uml.interaction.graph.tests/src/org/eclipse/papyrus/uml/interaction/tests/rules/ModelFixture.java b/tests/org.eclipse.papyrus.uml.interaction.graph.tests/src/org/eclipse/papyrus/uml/interaction/tests/rules/ModelFixture.java index aa5084a6..18b88c91 100644 --- a/tests/org.eclipse.papyrus.uml.interaction.graph.tests/src/org/eclipse/papyrus/uml/interaction/tests/rules/ModelFixture.java +++ b/tests/org.eclipse.papyrus.uml.interaction.graph.tests/src/org/eclipse/papyrus/uml/interaction/tests/rules/ModelFixture.java @@ -77,16 +77,22 @@ public class ModelFixture implements TestRule { private static final String SEQUENCE_DIAGRAM_TYPE = "LightweightSequenceDiagram"; //$NON-NLS-1$ + private static final String PAPYRUS_SEQUENCE_DIAGRAM_TYPE = "PapyrusUMLSequenceDiagram"; //$NON-NLS-1$ + private static final Set SEQUENCE_DIAGRAM_TYPES = new HashSet<>( Arrays.asList(SEQUENCE_DIAGRAM_TYPE, PAPYRUS_SEQUENCE_DIAGRAM_TYPE)); private final Class testClass; + private final String path; private ResourceSet rset; + private Package model; + private Interaction interaction; + private Optional sequenceDiagram; private Graph graph; @@ -95,12 +101,10 @@ public class ModelFixture implements TestRule { * Initializes me. * * @param testClass - * the test class in which context to load the resource. May be - * {@code null} for an ordinary {@code Rule} but required for a - * {@link ClassRule} + * the test class in which context to load the resource. May be {@code null} for an ordinary + * {@code Rule} but required for a {@link ClassRule} * @param path - * the path relative to the {@code testClass} of the UML resource to - * load + * the path relative to the {@code testClass} of the UML resource to load */ public ModelFixture(Class testClass, String path) { super(); @@ -157,10 +161,12 @@ public void evaluate() throws Throwable { protected void starting(Description description) { final String[] paths = getPaths(description); - for (String path : paths) { + for (@SuppressWarnings("hiding") + String path : paths) { URL resourceURL = getResourceURL(description, path); if (resourceURL == null) { fail("Resource not found: " + path); + return; // Unreachable } if (rset == null) { @@ -177,26 +183,28 @@ protected void starting(Description description) { String where = String.join(", ", paths); assertThat("No UML package found in " + where, model, notNullValue()); - interaction = (Interaction) UML2Util.findEObject(model.eAllContents(), + interaction = (Interaction)UML2Util.findEObject(model.eAllContents(), UMLPackage.Literals.INTERACTION::isInstance); assertThat("No UML interaction found in " + where, interaction, notNullValue()); // Look for a sequence diagram - sequenceDiagram = CacheAdapter.getCacheAdapter(interaction).getInverseReferences(interaction) - .stream() - .filter(setting -> setting - .getEStructuralFeature() == NotationPackage.Literals.VIEW__ELEMENT) + sequenceDiagram = CacheAdapter.getCacheAdapter(interaction).getInverseReferences(interaction).stream() + .filter(setting -> setting.getEStructuralFeature() == NotationPackage.Literals.VIEW__ELEMENT) .map(EStructuralFeature.Setting::getEObject).filter(Diagram.class::isInstance) - .map(Diagram.class::cast) - .filter(diagram -> SEQUENCE_DIAGRAM_TYPES.contains(diagram.getType())).findAny(); + .map(Diagram.class::cast).filter(this::isSequenceDiagram).findAny(); } - protected URL getResourceURL(Description description, String path) { + protected boolean isSequenceDiagram(Diagram diagram) { + return SEQUENCE_DIAGRAM_TYPES.contains(diagram.getType()); + } + + protected URL getResourceURL(Description description, @SuppressWarnings("hiding") String path) { Class context = testClass; if (context == null) { context = description.getTestClass(); if (context == null) { fail("Explicit test class required for @ClassRule"); + return null; // Unreachable } } @@ -211,6 +219,12 @@ protected ResourceSet createResourceSet() { return result; } + /** + * Clean up the described test. + * + * @param description + * a description of the test that finished + */ protected void finished(Description description) { interaction = null; model = null; @@ -225,7 +239,7 @@ protected String[] getPaths(Description description) { String[] result; if (path != null) { - result = new String[] { path }; + result = new String[] {path }; } else { result = requireAnnotation(description, ModelResource.class).value(); } @@ -233,8 +247,7 @@ protected String[] getPaths(Description description) { return result; } - protected final Optional getAnnotation(Description description, - Class type) { + protected final Optional getAnnotation(Description description, Class type) { A result = description.getAnnotation(type); if ((result == null) && (description.getTestClass() != null)) { result = description.getTestClass().getAnnotation(type); @@ -252,8 +265,7 @@ protected final A requireAnnotation(Description descripti } /** - * Compute the dependency graph of my default {@link #getInteraction() - * interaction}. + * Compute the dependency graph of my default {@link #getInteraction() interaction}. * * @return the dependency graph of my default interaction */ @@ -296,16 +308,14 @@ public NamedElement getElement(String qualifiedName) { * the qualified name of an element to get * @param type * the type of element to retrieve - * * @return the element - * * @param * the element type to retrieve */ public T getElement(String qualifiedName, Class type) { Collection result = UMLUtil.findNamedElements(rset, qualifiedName, false, - (EClass) UMLPackage.eINSTANCE.getEClassifier(type.getSimpleName())); + (EClass)UMLPackage.eINSTANCE.getEClassifier(type.getSimpleName())); assertThat("no such element: " + qualifiedName, result, hasItem(anything())); @@ -313,8 +323,7 @@ public T getElement(String qualifiedName, Class type } /** - * Get the vertex for a named element. Fails the test if the element is not - * found. + * Get the vertex for a named element. Fails the test if the element is not found. * * @param qualifiedName * the qualified name of an element which vertex to get @@ -409,8 +418,7 @@ public void describeTo(org.hamcrest.Description description) { // /** - * A specialized {@link ModelFixture} that provides an {@link EditingDomain} - * context for the test model. + * A specialized {@link ModelFixture} that provides an {@link EditingDomain} context for the test model. */ public static class Edit extends ModelFixture { /** @@ -434,12 +442,10 @@ public Edit(String path) { * Initializes me. * * @param testClass - * the test class in which context to load the resource. May be - * {@code null} for an ordinary {@code Rule} but required for a - * {@link ClassRule} + * the test class in which context to load the resource. May be {@code null} for an + * ordinary {@code Rule} but required for a {@link ClassRule} * @param path - * the path relative to the {@code testClass} of the UML resource to - * load + * the path relative to the {@code testClass} of the UML resource to load */ public Edit(Class testClass, String path) { super(testClass, path);