From 5867edc4d83fb50a7b110a65df579943e9f8cd88 Mon Sep 17 00:00:00 2001 From: "Christian W. Damus" Date: Mon, 19 Nov 2018 12:05:03 -0500 Subject: [PATCH 1/3] Issue #403 Update JUnit tests Update JUnit tests to account for new anchoring location of the delete message on the destruction occurrence shape. Signed-off-by: Christian W. Damus --- .../tests/LifelineSwitchingUITest.java | 71 ++++++++++--------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/LifelineSwitchingUITest.java b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/LifelineSwitchingUITest.java index d2e587eb..2f40b561 100644 --- a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/LifelineSwitchingUITest.java +++ b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/LifelineSwitchingUITest.java @@ -69,8 +69,7 @@ import org.junit.runners.JUnit4; /** - * Integration test cases for use cases in which occurrences are moved from one - * lifeline to another. + * Integration test cases for use cases in which occurrences are moved from one lifeline to another. * * @author Christian W. Damus */ @@ -175,8 +174,7 @@ protected void undoSwitchLifeline(VerificationMode mode) { public static class MessageNoReply extends MessageNoExecution { @ClassRule - public static LightweightSeqDPrefs prefs = new LightweightSeqDPrefs() - .dontCreateRepliesForSyncCalls(); + public static LightweightSeqDPrefs prefs = new LightweightSeqDPrefs().dontCreateRepliesForSyncCalls(); @Override protected void switchLifeline(VerificationMode mode) { @@ -291,7 +289,7 @@ protected void switchLifeline(VerificationMode mode) { super.switchLifeline(mode); // Verify additional details of the new visuals - EditPart destructionEP = ((ConnectionEditPart) messageEP).getTarget(); + EditPart destructionEP = ((ConnectionEditPart)messageEP).getTarget(); mode.verify(destructionEP, instanceOf(DestructionSpecificationEditPart.class)); EditPart newLifelineEP = verifying(mode, getBodyEditPart(getReceiver()), "New lifeline edit-part not found"); @@ -303,7 +301,7 @@ protected void undoSwitchLifeline(VerificationMode mode) { super.undoSwitchLifeline(mode); // Verify additional details of the old visuals - EditPart destructionEP = ((ConnectionEditPart) messageEP).getTarget(); + EditPart destructionEP = ((ConnectionEditPart)messageEP).getTarget(); mode.verify(destructionEP, instanceOf(DestructionSpecificationEditPart.class)); EditPart oldLifelineEP = verifying(mode, getBodyEditPart(getOriginalReceiver()), "Old lifeline edit-part not found"); @@ -311,8 +309,7 @@ protected void undoSwitchLifeline(VerificationMode mode) { } /** - * Verify that a destruction cannot be moved to a lifeline that would have - * occurrences following it + * Verify that a destruction cannot be moved to a lifeline that would have occurrences following it */ @Test public void existingOccurrences() { @@ -331,13 +328,13 @@ public void existingOccurrences() { // Verify that nothing changed assertThat(messageEP, runs(sendX, mesgY, getGrabX(), mesgY)); - EditPart destructionEP = ((ConnectionEditPart) messageEP).getTarget(); + EditPart destructionEP = ((ConnectionEditPart)messageEP).getTarget(); assertThat(destructionEP, instanceOf(DestructionSpecificationEditPart.class)); EditPart oldLifelineEP = asserting(getBodyEditPart(getOriginalReceiver()), "Old lifeline edit-part not found"); assertThat(destructionEP.getParent(), is(oldLifelineEP)); MDestruction destruction = asserting( - as(getInteraction().getElement((Element) destructionEP.getAdapter(EObject.class)), + as(getInteraction().getElement((Element)destructionEP.getAdapter(EObject.class)), MDestruction.class), "Not a logical destruction"); assertThat(asserting(destruction.getCovered(), "No coverage").getName(), is("Lifeline2")); @@ -354,12 +351,16 @@ IElementType getMessageType() { @Override int getGrabX() { - return super.getGrabX() - (DESTRUCTION_WIDTH / 2); + // Note that the message arrow actually penetrates to 1/4 depth; it does not attach + // to the bounding box of the X shape + return super.getGrabX() - (DESTRUCTION_WIDTH / 4); } @Override int getNewRecvX() { - return super.getNewRecvX() - (DESTRUCTION_WIDTH / 2); + // Note that the message arrow actually penetrates to 1/4 depth; it does not attach + // to the bounding box of the X shape + return super.getNewRecvX() - (DESTRUCTION_WIDTH / 4); } } @@ -399,8 +400,7 @@ protected void undoSwitchLifeline(VerificationMode mode) { } /** - * Verify that a creation cannot be moved to a lifeline that would have - * occurrences preceding it + * Verify that a creation cannot be moved to a lifeline that would have occurrences preceding it */ @Test public void existingOccurrences() { @@ -428,7 +428,7 @@ public void existingOccurrences() { assertThat(lifeline2, isBounded(anything(), is(top2), anything(), anything())); assertThat(lifeline3, isBounded(anything(), is(top3), anything(), anything())); MMessage message = assuming( - as(getInteraction().getElement((Element) messageEP.getAdapter(EObject.class)), + as(getInteraction().getElement((Element)messageEP.getAdapter(EObject.class)), MMessage.class), "Not a logical message"); MMessageEnd creation = assuming(message.getReceive(), "No creation end"); @@ -436,8 +436,8 @@ public void existingOccurrences() { } /** - * Verify that a create message can be reoriented to a lifeline head with the - * same outcome as the lifeline body. + * Verify that a create message can be reoriented to a lifeline head with the same outcome as the + * lifeline body. */ @Test public void switchToLifelineHead() { @@ -491,7 +491,9 @@ public static class ExecutionSpanningOccurrences extends MessageNoExecution { private static final int LIFELINE_4_BODY_X = 596; private final int m2Y = 173; + private final int m3Y = 198; + private final int m4Y = 223; @Override @@ -514,12 +516,12 @@ protected void switchLifeline(VerificationMode mode) { mode.verify(m4EP, runs(LIFELINE_4_BODY_X, m4Y, execRight, m4Y)); // And the semantics - mode.verify("Wrong coverage of m2 send", - asserting(m2.getSend(), "m2 lost its send").getCovered(), is(getReceiver())); + mode.verify("Wrong coverage of m2 send", asserting(m2.getSend(), "m2 lost its send").getCovered(), + is(getReceiver())); mode.verify("Coverage of m2 receive broken", asserting(m2.getReceive(), "m2 lost its recevive").getCovered(), is(getReceiver())); - mode.verify("Wrong coverage of m3 send", - asserting(m3.getSend(), "m3 lost its send").getCovered(), is(getReceiver())); + mode.verify("Wrong coverage of m3 send", asserting(m3.getSend(), "m3 lost its send").getCovered(), + is(getReceiver())); mode.verify("Wrong coverage of m4 receive", asserting(m4.getReceive(), "m4 lost its receive").getCovered(), is(getReceiver())); } @@ -542,12 +544,12 @@ protected void undoSwitchLifeline(VerificationMode mode) { mode.verify(m4EP, runs(LIFELINE_4_BODY_X, m4Y, execRight, m4Y)); // And the semantics - mode.verify("Wrong coverage of m2 send", - asserting(m2.getSend(), "m2 lost its send").getCovered(), is(getOriginalReceiver())); + mode.verify("Wrong coverage of m2 send", asserting(m2.getSend(), "m2 lost its send").getCovered(), + is(getOriginalReceiver())); mode.verify("Coverage of m2 receive broken", asserting(m2.getReceive(), "m2 lost its recevive").getCovered(), is(getReceiver())); - mode.verify("Wrong coverage of m3 send", - asserting(m3.getSend(), "m3 lost its send").getCovered(), is(getOriginalReceiver())); + mode.verify("Wrong coverage of m3 send", asserting(m3.getSend(), "m3 lost its send").getCovered(), + is(getOriginalReceiver())); mode.verify("Wrong coverage of m4 receive", asserting(m4.getReceive(), "m4 lost its receive").getCovered(), is(getOriginalReceiver())); @@ -665,8 +667,7 @@ MMessage requireMessage(String name) { EditPart requireEditPart(MElement element) { View notationView = assuming(as(element.getDiagramView(), View.class), "No notation view for " + element); - EditPart result = DiagramEditPartsUtil.getEditPartFromView(notationView, - editor.getDiagramEditPart()); + EditPart result = DiagramEditPartsUtil.getEditPartFromView(notationView, editor.getDiagramEditPart()); assumeThat("No edit-part for " + element, result, notNullValue()); return result; } @@ -688,10 +689,10 @@ EditPart forceCreateNode(EditPart parent, IElementType type, Point location, Dim editor.flushDisplayEvents(); - View createdView = (View) create.getViewAndElementDescriptor().getAdapter(View.class); + View createdView = (View)create.getViewAndElementDescriptor().getAdapter(View.class); assumeThat("No view created", createdView, notNullValue()); - EditPart result = (EditPart) editor.getDiagramEditPart().getViewer().getEditPartRegistry() + EditPart result = (EditPart)editor.getDiagramEditPart().getViewer().getEditPartRegistry() .get(createdView); assumeThat("No edit-part registered for created view", result, notNullValue()); return result; @@ -717,12 +718,12 @@ static T asserting(Optional value, String message) { static T verifying(VerificationMode mode, Optional value, String message) { switch (mode) { - case ASSERT: - return asserting(value, message); - case ASSUME: - return assuming(value, message); - default: - return value.orElse(null); + case ASSERT: + return asserting(value, message); + case ASSUME: + return assuming(value, message); + default: + return value.orElse(null); } } } From cd9f876d999d21557e4c02d5249d59ac6bfc3c2c Mon Sep 17 00:00:00 2001 From: "Christian W. Damus" Date: Fri, 16 Nov 2018 08:39:20 -0500 Subject: [PATCH 2/3] Fix #411 Move nested executions - Handle moving an execution to make it nested in another - Handle moving an execution to make it nest another - Ensure that the user cannot reshape execution specifications to effect partial nesting (incomplete overlap of extent) - Indirectly fix #422 redundant nudging of nested executions Signed-off-by: Christian W. Damus --- ...cutionSpecificationCreationEditPolicy.java | 9 +- .../ExecutionSpecificationDropEditPolicy.java | 42 ++++ .../policies/LifelineBodyDropEditPolicy.java | 16 +- .../policies/NoPapyrusEditPolicyProvider.java | 6 +- .../edit/policies/PrivateRequestUtils.java | 29 ++- .../internal/model/commands/NudgeCommand.java | 27 ++- .../model/commands/PendingChildData.java | 73 +++---- .../PendingContainmentChangeData.java | 178 +++++++++++++++++ .../model/commands/PendingNestedData.java | 83 ++++++++ .../model/commands/SetCoveredCommand.java | 5 + .../model/commands/SetOwnerCommand.java | 34 ++-- .../model/spi/impl/DefaultDiagramHelper.java | 25 +++ .../interaction/model/util/Executions.java | 58 +++++- .../model/util/LogicalModelPredicates.java | 14 ++ .../uml/interaction/model/util/Optionals.java | 33 ++++ ...tionSpecificationDragEditPolicyUITest.java | 4 + .../tests/NestedExecutionMoveUITest.java | 186 ++++++++++++++++++ .../tests/ShrinkExpandExecutionUITest.java | 88 ++++++--- .../edit/policies/tests/nested-executions.di | 2 + .../policies/tests/nested-executions.notation | 76 +++++++ .../edit/policies/tests/nested-executions.uml | 27 +++ .../runtime/tests/rules/AutoFixture.java | 17 ++ .../runtime/tests/rules/AutoFixtureRule.java | 147 ++++++++++---- .../tests/LogicalModelPredicatesTest.java | 25 +++ 24 files changed, 1040 insertions(+), 164 deletions(-) create mode 100644 plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/ExecutionSpecificationDropEditPolicy.java create mode 100644 plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingContainmentChangeData.java create mode 100644 plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingNestedData.java create mode 100644 tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/NestedExecutionMoveUITest.java create mode 100644 tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/nested-executions.di create mode 100644 tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/nested-executions.notation create mode 100644 tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/nested-executions.uml diff --git a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/ExecutionSpecificationCreationEditPolicy.java b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/ExecutionSpecificationCreationEditPolicy.java index 29098bb5..f4be3eff 100644 --- a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/ExecutionSpecificationCreationEditPolicy.java +++ b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/ExecutionSpecificationCreationEditPolicy.java @@ -30,6 +30,7 @@ import org.eclipse.papyrus.uml.interaction.model.MExecution; import org.eclipse.papyrus.uml.interaction.model.MInteraction; import org.eclipse.papyrus.uml.interaction.model.MObject; +import org.eclipse.papyrus.uml.interaction.model.spi.ViewTypes; import org.eclipse.papyrus.uml.interaction.model.util.Optionals; import org.eclipse.papyrus.uml.interaction.model.util.SequenceDiagramSwitch; import org.eclipse.uml2.uml.Element; @@ -74,8 +75,12 @@ public Command caseExecutionSpecification(IElementType type) { } } - return execution.insertNestedExecutionAfter(before.orElse(execution), offset, - size != null ? size.height : 40, eClass); + return execution + .insertNestedExecutionAfter(before.orElse(execution), offset, + size != null ? size.height + : getLayoutConstraints() + .getMinimumHeight(ViewTypes.EXECUTION_SPECIFICATION), + eClass); } @Override diff --git a/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/ExecutionSpecificationDropEditPolicy.java b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/ExecutionSpecificationDropEditPolicy.java new file mode 100644 index 00000000..57feb80a --- /dev/null +++ b/plugins/org.eclipse.papyrus.uml.diagram.sequence.runtime/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/ExecutionSpecificationDropEditPolicy.java @@ -0,0 +1,42 @@ +/***************************************************************************** + * 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 org.eclipse.papyrus.uml.interaction.model.MExecution; +import org.eclipse.papyrus.uml.interaction.model.MLifeline; +import org.eclipse.uml2.uml.ExecutionSpecification; + +/** + * Edit policy for dropping elements onto an execution specification. + */ +public class ExecutionSpecificationDropEditPolicy extends LifelineBodyDropEditPolicy { + + /** + * Initializes me. + */ + public ExecutionSpecificationDropEditPolicy() { + super(); + } + + @Override + protected Optional getHostLifeline() { + Optional execution = as(Optional.ofNullable(getHostObject()), + ExecutionSpecification.class); + return as(execution.flatMap(getInteraction()::getElement), MExecution.class) + .map(MExecution::getOwner); + } +} 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 index 719deeb3..7ec7d976 100644 --- 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 @@ -12,14 +12,20 @@ package org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.edit.policies; +import static org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.edit.policies.PrivateRequestUtils.getChangeBoundsRequest; import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.as; +import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.flatMap; +import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.map; +import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.mapToInt; import java.util.Optional; import java.util.OptionalInt; +import org.eclipse.draw2d.geometry.Point; import org.eclipse.emf.ecore.EObject; import org.eclipse.gef.commands.Command; import org.eclipse.gef.commands.UnexecutableCommand; +import org.eclipse.gef.requests.ChangeBoundsRequest; 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; @@ -65,13 +71,19 @@ protected Command getDropElementCommand(EObject element, DropObjectsRequest requ protected Command getDropExecutionCommand(MExecution execution, DropObjectsRequest request) { Optional lifeline = getHostLifeline(); + // FIXME: Must allow non-reordering changes 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); + Optional changeBounds = Optional.ofNullable(getChangeBoundsRequest(request)) + .map(ChangeBoundsRequest::getMoveDelta); + OptionalInt deltaY = mapToInt(changeBounds, Point::y); + OptionalInt top = flatMap(execution.getTop(), t -> map(deltaY, y -> t + y)); + OptionalInt bottom = flatMap(execution.getBottom(), b -> map(deltaY, y -> b + y)); + + return lifeline.map(ll -> wrap(execution.setOwner(ll, top, bottom))).orElse(null); } protected Optional getHostLifeline() { 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 02148eb1..9811f171 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 @@ -59,8 +59,8 @@ public NoPapyrusEditPolicyProvider() { withDrop(LifelineCreationEditPolicy::new, LifelineBodyDropEditPolicy::new)); substitute(InteractionCompartmentEditPart.class, EditPolicyRoles.CREATION_ROLE, InteractionCreationEditPolicy::new); - substitute(ExecutionSpecificationEditPart.class, EditPolicyRoles.CREATION_ROLE, - ExecutionSpecificationCreationEditPolicy::new); + substitute(ExecutionSpecificationEditPart.class, EditPolicyRoles.CREATION_ROLE, withDrop( + ExecutionSpecificationCreationEditPolicy::new, ExecutionSpecificationDropEditPolicy::new)); // Diagram assistant edit policies substitute(LifelineBodyEditPart.class, EditPolicyRoles.POPUPBAR_ROLE, @@ -164,11 +164,13 @@ public void setHost(EditPart host) { super.setHost(host); } + @SuppressWarnings("unchecked") @Override protected DropObjectsRequest castToDropObjectsRequest(ChangeBoundsRequest request) { DropObjectsRequest result = super.castToDropObjectsRequest(request); PrivateRequestUtils.forwardParameters(request, result); + PrivateRequestUtils.setChangeBoundsRequest(result, request); 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 cec7c6e2..a54382c4 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 @@ -17,6 +17,8 @@ import org.eclipse.draw2d.geometry.Point; import org.eclipse.gef.Request; +import org.eclipse.gef.requests.ChangeBoundsRequest; +import org.eclipse.gmf.runtime.diagram.ui.requests.DropObjectsRequest; /** * Utilities for working with private {@link Request}s and/or private details of {@code Request}s. @@ -32,10 +34,12 @@ public final class PrivateRequestUtils { private static final String ALLOW_SEMANTIC_REORDERING_PARAMETER = "__allow_semantic_reordering__"; //$NON-NLS-1$ + private static final String CHANGE_BOUNDS_REQUEST_PARAMETER = "__change_bounds_request__"; //$NON-NLS-1$ + private static final String[] PARAMETERS = { // FORCE_PARAMETER, // ORIGINAL_MOUSE_PARAMETER, ORIGINAL_SOURCE_PARAMETER, ORIGINAL_TARGET_PARAMETER, // - ALLOW_SEMANTIC_REORDERING_PARAMETER, // + ALLOW_SEMANTIC_REORDERING_PARAMETER, CHANGE_BOUNDS_REQUEST_PARAMETER, // }; /** @@ -198,4 +202,27 @@ public static void forwardParameters(Request fromRequest, Request toRequest) { .filter(p -> !hasParameter(toRequest, p)) .forEach(p -> setParameter(toRequest, p, getParameter(fromRequest, p, Object.class, null))); } + + /** + * Records the change-bounds request that triggers a {@code drop}. + * + * @param drop + * a drop request + * @param changeBounds + * the change-bounds request that triggered it + */ + public static void setChangeBoundsRequest(DropObjectsRequest drop, ChangeBoundsRequest changeBounds) { + setParameter(drop, CHANGE_BOUNDS_REQUEST_PARAMETER, changeBounds); + } + + /** + * Queries the change-bounds request that triggers a {@code drop}. + * + * @param drop + * a drop request + * @return the change-bounds request that triggered it + */ + public static ChangeBoundsRequest getChangeBoundsRequest(DropObjectsRequest drop) { + return getParameter(drop, CHANGE_BOUNDS_REQUEST_PARAMETER, ChangeBoundsRequest.class, null); + } } diff --git a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/NudgeCommand.java b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/NudgeCommand.java index 40db365f..57922387 100644 --- a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/NudgeCommand.java +++ b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/NudgeCommand.java @@ -118,7 +118,7 @@ private class MoveDownVisitor extends CommandBuildingVisitor { private final int delta; - private Set movingLifelineBodies = new HashSet<>(); + private Set movingShapes = new HashSet<>(); MoveDownVisitor(int delta) { super(); @@ -134,10 +134,13 @@ protected void process(Vertex vertex) { if (view instanceof Shape) { // Move the shape down Shape shape = (Shape)view; - if (!isShapeOnMovingLifeline(shape)) { + if (!isShapeOnMovingShape(shape)) { int top = layoutHelper().getTop(shape); chain(layoutHelper().setTop(shape, top + delta)); } + + // Whether we moved it, or it's moving indirectly, it's moving + movingShapes.add(shape); } else if (view instanceof Connector && element instanceof Message) { // Move connector anchors down Connector connector = (Connector)view; @@ -163,12 +166,12 @@ protected void process(Vertex vertex) { .map(Vertex.class::cast); // Record that we're moving this lifeline.map(Vertex::getDiagramView).map(diagramHelper()::getLifelineBodyShape) - .ifPresent(movingLifelineBodies::add); + .ifPresent(movingShapes::add); Optional shape = lifeline.map(Vertex::getDiagramView).map(Shape.class::cast); shape.ifPresent(s -> chain( layoutHelper().setTop(shape.get(), layoutHelper().getTop(s) + delta))); } else if (targetVtx.hasTag(Tag.LIFELINE_DESTRUCTION) - && !isShapeOnMovingLifeline(targetOn)) { + && !isShapeOnMovingShape(targetOn)) { // Move Destruction Shape if it's not moved implicitly by the lifeline it's on Optional shape = Optional.ofNullable(targetVtx.getDiagramView()) .filter(Shape.class::isInstance).map(Shape.class::cast); @@ -229,12 +232,20 @@ private boolean isMessageEndNudgeCommand(Element element) { */ private boolean skipMove(Shape shape) { return !ViewTypes.LIFELINE_BODY.equals(shape.getType()) // - || movingLifelineBodies.contains(shape) // - || isShapeOnMovingLifeline(shape); + || movingShapes.contains(shape) // + || isShapeOnMovingShape(shape); } - private boolean isShapeOnMovingLifeline(Shape shape) { - return movingLifelineBodies.contains(shape.eContainer()); + /** + * Is a shape on (a child of) a shape that is already being nudged, and so doesn't need to be nudged, + * itself? + * + * @param shape + * a shape to be nudged (perhaps) + * @return whether it is indirectly being nudged by being a child of a shape being nudged + */ + private boolean isShapeOnMovingShape(Shape shape) { + return movingShapes.contains(shape.eContainer()); } } diff --git a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingChildData.java b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingChildData.java index 896bddae..198b47f0 100644 --- a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingChildData.java +++ b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingChildData.java @@ -12,9 +12,11 @@ package org.eclipse.papyrus.uml.interaction.internal.model.commands; +import java.util.List; import java.util.Optional; import org.eclipse.papyrus.uml.interaction.model.MElement; +import org.eclipse.papyrus.uml.interaction.model.MExecution; import org.eclipse.uml2.uml.Element; /** @@ -22,46 +24,32 @@ * * @see DependencyContext */ -public final class PendingChildData { - private final MElement owner; - - private final MElement child; +public final class PendingChildData extends PendingContainmentChangeData, MElement> { private PendingChildData(MElement owner, MElement child) { - super(); - - this.owner = owner; - this.child = child; + super(owner, child); } /** - * Queries the owner of the pending {@linkplain #getPendingChild() owned element}. + * Query the element that is in process of having the given element set as its new {@code owner}. * - * @return the owner element + * @param owner + * an element that may or may not be getting a new owned child + * @return the pending child element */ - public MElement getOwner() { - return owner; + public static Optional> getPendingChild(MElement owner) { + return PendingContainmentChangeData.getPendingChild(PendingChildData.class, owner); } /** - * Queries the element that is being assigned a {@linkplain #getOwner() new owner}. - * - * @return the owned element - */ - public MElement getPendingChild() { - return child; - } - - /** - * Query the element that is in process of having the given element set as its new {@code owner}. + * Query the elements that are in process of having the given element set as their new {@code owner}. * * @param owner * an element that may or may not be getting a new owned child - * @return the pending child element + * @return the owned elements */ - public static Optional> getPendingChild(MElement owner) { - return DependencyContext.get().get(owner, PendingChildData.class) - .map(PendingChildData::getPendingChild); + public static List> getPendingChildren(MElement owner) { + return PendingContainmentChangeData.getPendingChildren(PendingChildData.class, owner); } /** @@ -72,8 +60,7 @@ public static Optional> getPendingChild(MElement> getPendingOwner(MElement child) { - return DependencyContext.get().get(child, PendingOwnerData.class) - .map(PendingOwnerData::getPendingOwner); + return PendingContainmentChangeData.getPendingParent(PendingChildData.class, child); } /** @@ -86,28 +73,14 @@ public static Optional> getPendingOwner(MElement owner, MElement pendingChild) { if (owner.getElement() != pendingChild.getOwner().getElement()) { - DependencyContext ctx = DependencyContext.get(); - PendingChildData childData = new PendingChildData(owner, pendingChild); - ctx.put(owner, childData); - ctx.put(pendingChild, new PendingOwnerData(childData)); - } - } - - // - // Nested types - // - - private static final class PendingOwnerData { - private final PendingChildData childData; - - PendingOwnerData(PendingChildData childData) { - super(); - - this.childData = childData; - } - - MElement getPendingOwner() { - return childData.getOwner(); + PendingContainmentChangeData.setPendingChild(PendingChildData.class, owner, pendingChild, + PendingChildData::new); + + if (pendingChild instanceof MExecution) { + // Its nested executions also get the new owner lifeline + ((MExecution)pendingChild).getNestedExecutions() + .forEach(nested -> setPendingChild(owner, nested)); + } } } diff --git a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingContainmentChangeData.java b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingContainmentChangeData.java new file mode 100644 index 00000000..63e49cae --- /dev/null +++ b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingContainmentChangeData.java @@ -0,0 +1,178 @@ +/***************************************************************************** + * 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.interaction.internal.model.commands; + +import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.ifPresentElse; + +import com.google.common.collect.Lists; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.BiFunction; + +import org.eclipse.papyrus.uml.interaction.model.MElement; +import org.eclipse.uml2.uml.Element; + +/** + * A framework for {@linkplain DependencyContext context} variables that record logical, semantic, and/or + * visual containment changes pending in the model operation currently being computed. + * + * @see DependencyContext + */ +abstract class PendingContainmentChangeData

, C extends MElement> { + private final P parent; + + private final List children = Lists.newArrayListWithExpectedSize(1); + + PendingContainmentChangeData(P parent, C child) { + super(); + + this.parent = parent; + addChild(child); + } + + private void addChild(C child) { + children.add(child); + } + + /** + * Queries the parent of the pending {@linkplain #getPendingChild() child element}. + * + * @return the parent element + */ + public P getParent() { + return parent; + } + + /** + * Queries the element that is being assigned a {@linkplain #getParent() new parent}. + * + * @return the child element + */ + public C getPendingChild() { + return children.get(0); + } + + /** + * Queries the elements that are being assigned a {@linkplain #getParent() new parent}. + * + * @return the child elements + */ + public List getPendingChildren() { + return Collections.unmodifiableList(children); + } + + /** + * Query the element that is in process of having the given element set as its new {@code parent}. + * + * @param parent + * an element that may or may not be getting a new child + * @return the pending child element + */ + static

, C extends MElement> Optional getPendingChild( + Class> relationshipType, P parent) { + + return DependencyContext.get().get(parent, relationshipType) + .map(PendingContainmentChangeData::getPendingChild); + } + + /** + * Query the elements that are in process of having the given element set as their new {@code parent}. + * + * @param parent + * an element that may or may not be getting a new child + * @return the pending child elements + */ + static

, C extends MElement> List getPendingChildren( + Class> relationshipType, P parent) { + return DependencyContext.get().get(parent, relationshipType) + .map(PendingContainmentChangeData::getPendingChildren).orElse(Collections.emptyList()); + } + + /** + * Query the element that is in process of being made the parent of a pending {@code child}. + * + * @param child + * an element that may or may not be getting a new parent + * @return the pending parent element + */ + static

, C extends MElement> Optional

getPendingParent( + Class> relationshipType, C child) { + return getPendingParentData(DependencyContext.get(), relationshipType, child) + .map(PendingParentData::getPendingParent); + } + + @SuppressWarnings({"unchecked", "rawtypes" }) + private static

, C extends MElement> Optional> getPendingParentData( + DependencyContext ctx, Class> relationshipType, + C child) { + + return (Optional)ctx.get(child, PendingParentData.class, + data -> data.getRelationshipType() == relationshipType); + } + + /** + * Record an element that is in process of having the given element set as its new {@code owner}. + * + * @param owner + * an element that is getting a new owned child + * @param pendingChild + * an element that is getting the new {@code owner} + */ + static

, C extends MElement> void setPendingChild( + Class> relationshipType, P parent, C pendingChild, + BiFunction> factory) { + + DependencyContext ctx = DependencyContext.get(); + + @SuppressWarnings({"unchecked", "rawtypes" }) + Optional> childData = (Optional)ctx.get(parent, relationshipType); + + ifPresentElse(childData, cd -> { + cd.addChild(pendingChild); + ctx.put(pendingChild, new PendingParentData<>(relationshipType, cd)); + }, () -> { + PendingContainmentChangeData cd = factory.apply(parent, pendingChild); + ctx.put(parent, cd); + ctx.put(pendingChild, new PendingParentData<>(relationshipType, cd)); + }); + } + + // + // Nested types + // + + private static final class PendingParentData

, C extends MElement> { + private final Class> relationshipType; + + private final PendingContainmentChangeData childData; + + PendingParentData(Class> relationshipType, + PendingContainmentChangeData childData) { + super(); + + this.relationshipType = relationshipType; + this.childData = childData; + } + + Class> getRelationshipType() { + return relationshipType; + } + + P getPendingParent() { + return childData.getParent(); + } + } + +} diff --git a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingNestedData.java b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingNestedData.java new file mode 100644 index 00000000..3998a2f9 --- /dev/null +++ b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingNestedData.java @@ -0,0 +1,83 @@ +/***************************************************************************** + * 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.interaction.internal.model.commands; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.papyrus.uml.interaction.model.MExecution; + +/** + * A {@linkplain DependencyContext context} variable that records execution nesting change in progress. + * + * @see DependencyContext + */ +public final class PendingNestedData extends PendingContainmentChangeData { + private PendingNestedData(MExecution nesting, MExecution nested) { + super(nesting, nested); + } + + /** + * Query the execution that is in process of having the given element set as its new {@code nesting} + * execution. + * + * @param nesting + * an execution that may or may not be getting a new nested execution + * @return the pending nested execution + */ + public static Optional getPendingChild(MExecution nesting) { + return PendingContainmentChangeData.getPendingChild(PendingNestedData.class, nesting); + } + + /** + * Query the executions that are in process of having the given execution set as their new + * {@code newsting}. + * + * @param nesting + * an execution that may or may not be getting a new nested execution + * @return the pending nested executions + */ + public static List getPendingNestedExecutions(MExecution nesting) { + return PendingContainmentChangeData.getPendingChildren(PendingNestedData.class, nesting); + } + + /** + * Query the execution that is in process of being made the nesting of a pending {@code nested} execution. + * + * @param nested + * an execution that may or may not be getting a new nesting execution + * @return the pending nesting execution + */ + public static Optional getPendingNestingExecution(MExecution nested) { + return PendingContainmentChangeData.getPendingParent(PendingNestedData.class, nested); + } + + /** + * Record an execution that is in process of having the given execution set as its new {@code nesting} + * execution. + * + * @param nesting + * an execution that is getting a new nested execution + * @param pendingNested + * an execution that is getting the new {@code nesting} + */ + static void setPendingNested(MExecution nesting, MExecution pendingNested) { + if (!nesting.getNestedExecutions().stream() + .anyMatch(n -> n.getElement() == pendingNested.getElement())) { + + PendingContainmentChangeData.setPendingChild(PendingNestedData.class, nesting, pendingNested, + PendingNestedData::new); + } + } + +} 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 9a426d1e..c9005d96 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 @@ -15,6 +15,7 @@ import static java.lang.Math.abs; import static java.util.Collections.singletonList; import static org.eclipse.papyrus.uml.interaction.model.util.Executions.executionShapeAt; +import static org.eclipse.papyrus.uml.interaction.model.util.Executions.splitsExecution; import static org.eclipse.papyrus.uml.interaction.model.util.LogicalModelPredicates.above; import static org.eclipse.papyrus.uml.interaction.model.util.LogicalModelPredicates.below; import static org.eclipse.papyrus.uml.interaction.model.util.LogicalModelPredicates.equalTo; @@ -177,6 +178,10 @@ protected Command doCreateCommand() { // And re-connect the message view to the new lifeline's head result = chain(result, defer(() -> reconnectTarget(messageView, lifelineHead, createPosition).orElse(null))); + } else if ((getTarget() instanceof MExecutionOccurrence) + && getTarget().getExecution().filter(splitsExecution(lifeline)).isPresent()) { + + result = UnexecutableCommand.INSTANCE; } else { // Handle a dependent execution result = dependencies(getTarget()).map(chaining(result)).orElse(result); 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 e2552fdb..9bab0e54 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 @@ -14,7 +14,8 @@ import static java.util.Collections.singletonList; 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.Executions.executionShapeAt; +import static org.eclipse.papyrus.uml.interaction.model.util.LogicalModelPredicates.equalTo; import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.as; import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.lessThan; import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.map; @@ -28,7 +29,6 @@ import org.eclipse.emf.common.command.UnexecutableCommand; import org.eclipse.emf.ecore.EObject; import org.eclipse.gmf.runtime.notation.Shape; -import org.eclipse.gmf.runtime.notation.View; import org.eclipse.papyrus.uml.interaction.internal.model.impl.MElementImpl; import org.eclipse.papyrus.uml.interaction.model.MElement; import org.eclipse.papyrus.uml.interaction.model.MExecution; @@ -89,22 +89,16 @@ protected boolean isChangingOwner() { return owner.getElement() != newOwner.getElement(); } + protected boolean isChangingNesting() { + return (getTarget() instanceof MExecution) + && PendingNestedData.getPendingNestingExecution((MExecution)getTarget()).isPresent(); + } + protected boolean isChangingPosition() { return (top.isPresent() && !getTarget().getTop().equals(top)) || (bottom.isPresent() && !getTarget().getBottom().equals(bottom)); } - protected boolean needReparentView(MExecution execution) { - // browse the list of parent views until we get to the real lifeline view. - // once found, compare to the "new" lifeline owner. - // if similar, no need to reparent - // this avoids issues in reparenting executions with nested executions - Optional parentLifelineView = getLifelineView(execution); - return parentLifelineView.isPresent() - && !parentLifelineView.equals(as(newOwner.getDiagramView(), View.class)); - - } - @Override protected Command doCreateCommand() { return new SequenceDiagramSwitch() { @@ -142,12 +136,12 @@ protected Command createCommand(MExecution execution, MLifeline lifeline) { int newTop = top.orElseGet(() -> execution.getTop().getAsInt()); int newBottom = bottom.orElseGet(() -> execution.getBottom().getAsInt()); - if (isChangingOwner() && needReparentView(execution)) { - // Move the execution shape - result = chain(result, diagramHelper().reparentView(executionShape.get(), lifelineView)); - } + if (isChangingPosition() || isChangingOwner() || isChangingNesting()) { + // Handle new parent shape (moving to/from nested condition) + Optional nestingExecution = executionShapeAt(lifeline, newTop, equalTo(execution)); + Shape newParent = nestingExecution.orElse(lifelineView); + result = chain(result, diagramHelper().reparentView(executionShape.get(), newParent)); - if (isChangingPosition() || isChangingOwner()) { // Position it later, as the relative position of the execution may then be different // according to the new lifeline's creation position result = chain(result, layoutHelper().setTop(executionShape.get(), () -> newTop)); @@ -195,6 +189,10 @@ protected Optional dependencies(MExecution execution, MLifeline lifelin // Note that nested executions will be handled implicitly by either their start or finish. Optional reattach = affectedOccurrences(execution).map(occ -> { + // If our 'execution' is now nesting another, let it know that + Optionals.elseMaybe(occ.getStartedExecution(), occ.getFinishedExecution()) + .ifPresent(nested -> PendingNestedData.setPendingNested(execution, nested)); + OptionalInt where = occ.getTop(); return defer(() -> occ.setCovered(lifeline, where)); }).reduce(chaining()); diff --git a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/spi/impl/DefaultDiagramHelper.java b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/spi/impl/DefaultDiagramHelper.java index 33065fdb..18276a7e 100644 --- a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/spi/impl/DefaultDiagramHelper.java +++ b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/spi/impl/DefaultDiagramHelper.java @@ -526,12 +526,37 @@ public Command deleteView(EObject diagramView) { return DeleteCommand.create(editingDomain, diagramView); } + @SuppressWarnings("boxing") @Override public Command reparentView(View view, View newParent) { + if (view.eContainer() == newParent) { + // Nothing to do + return IdentityCommand.INSTANCE; + } + // First record the original containment by removing the view Command result = RemoveCommand.create(editingDomain, view); result = result.chain(AddCommand.create(editingDomain, newParent, NotationPackage.Literals.VIEW__PERSISTED_CHILDREN, view)); + + if (ViewTypes.EXECUTION_SPECIFICATION.equals(view.getType()) && (view instanceof Node) + && (newParent instanceof Node)) { + // Ensure the correct horizontal alignment + Bounds bounds = (Bounds)((Node)view).getLayoutConstraint(); + + if (ViewTypes.EXECUTION_SPECIFICATION.equals(newParent.getType())) { + // Center on the parent's right edge + Bounds parentBounds = (Bounds)((Node)newParent).getLayoutConstraint(); + result = result + .chain(SetCommand.create(editingDomain, bounds, NotationPackage.Literals.LOCATION__X, + parentBounds.getWidth() - (bounds.getWidth() / 2))); + } else if (ViewTypes.LIFELINE_BODY.equals(newParent.getType())) { + // Center on the lifeline stem + result = result.chain(SetCommand.create(editingDomain, bounds, + NotationPackage.Literals.LOCATION__X, -bounds.getWidth() / 2)); + } + } + return result; } diff --git a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/model/util/Executions.java b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/model/util/Executions.java index c7802d99..303beb54 100644 --- a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/model/util/Executions.java +++ b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/model/util/Executions.java @@ -1,8 +1,13 @@ package org.eclipse.papyrus.uml.interaction.model.util; +import static org.eclipse.papyrus.uml.interaction.model.util.LogicalModelPredicates.alwaysFalse; +import static org.eclipse.papyrus.uml.interaction.model.util.LogicalModelPredicates.equalTo; import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.as; +import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.elseMaybe; +import static org.eclipse.papyrus.uml.interaction.model.util.Optionals.flatMapToObj; import java.util.Optional; +import java.util.OptionalInt; import java.util.function.Predicate; import org.eclipse.emf.ecore.EObject; @@ -10,9 +15,11 @@ import org.eclipse.gmf.runtime.notation.View; import org.eclipse.papyrus.uml.interaction.internal.model.commands.PendingChildData; import org.eclipse.papyrus.uml.interaction.internal.model.commands.PendingVerticalExtentData; +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.spi.ViewTypes; +import org.eclipse.uml2.uml.Element; public class Executions { @@ -40,21 +47,56 @@ protected static Optional parentLifelineView(Optional view) { } public static Optional executionAt(MLifeline lifeline, int absoluteY) { - Predicate spanning = PendingVerticalExtentData.spans(absoluteY); + return executionAt(lifeline, absoluteY, alwaysFalse()); + } - Optional movingToLifeline = as(PendingChildData.getPendingChild(lifeline), - MExecution.class).filter(spanning); + public static Optional executionAt(MLifeline lifeline, int absoluteY, + Predicate excluding) { - if (movingToLifeline.isPresent()) { - // Easy case - return movingToLifeline; - } + Predicate spanning = PendingVerticalExtentData. spans(absoluteY) + .and(excluding.negate()); + + Optional movingToLifeline = PendingChildData.getPendingChildren(lifeline).stream() + .filter(MExecution.class::isInstance).map(MExecution.class::cast).filter(spanning) + .max(LogicalModelOrdering.vertically()); - return lifeline.getExecutions().stream().filter(spanning).max(LogicalModelOrdering.vertically()); + return elseMaybe(movingToLifeline, // Easy case + () -> lifeline.getExecutions().stream().filter(spanning) + .max(LogicalModelOrdering.vertically())); } public static Optional executionShapeAt(MLifeline lifeline, int absoluteY) { return executionAt(lifeline, absoluteY).flatMap(MExecution::getDiagramView); } + public static Optional executionShapeAt(MLifeline lifeline, int absoluteY, + Predicate excluding) { + return executionAt(lifeline, absoluteY, excluding).flatMap(MExecution::getDiagramView); + } + + /** + * Obtain a predicate that tests whether an {@link MExecution} start or finish splits some execution on a + * given lifeline. An execution is split by another if exactly one end is spanned by the other execution. + * + * @param onLifeline + * the lifeline on which to look for split executions + * @return the execution splitting predicate + */ + public static Predicate splitsExecution(MLifeline onLifeline) { + return exec -> { + OptionalInt start = PendingVerticalExtentData.getPendingTop(exec); + OptionalInt finish = PendingVerticalExtentData.getPendingBottom(exec); + Predicate> self = equalTo(exec); + + Optional startLandsOn = flatMapToObj(start, + top -> executionAt(onLifeline, top, self)); + Optional finishLandsOn = flatMapToObj(finish, + bottom -> executionAt(onLifeline, bottom, self)); + + return (startLandsOn.isPresent() != finishLandsOn.isPresent()) // + || (finishLandsOn.isPresent() + && !startLandsOn.filter(equalTo(finishLandsOn.get())).isPresent()); + }; + } + } diff --git a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/model/util/LogicalModelPredicates.java b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/model/util/LogicalModelPredicates.java index 6131608a..996cc6d3 100644 --- a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/model/util/LogicalModelPredicates.java +++ b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/model/util/LogicalModelPredicates.java @@ -26,6 +26,10 @@ */ public class LogicalModelPredicates { + private static final Predicate TRUE = __ -> true; + + private static final Predicate FALSE = __ -> false; + /** * Not instantiable by clients. */ @@ -33,6 +37,16 @@ private LogicalModelPredicates() { super(); } + @SuppressWarnings("unchecked") + public static > Predicate alwaysTrue() { + return (Predicate)TRUE; + } + + @SuppressWarnings("unchecked") + public static > Predicate alwaysFalse() { + return (Predicate)FALSE; + } + public static Predicate> above(int y) { return self -> self.getTop().orElse(Integer.MAX_VALUE) < y; } 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 8374baec..ad320366 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 @@ -14,6 +14,7 @@ import java.util.Optional; import java.util.OptionalInt; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.IntFunction; import java.util.function.IntPredicate; @@ -71,6 +72,19 @@ public static OptionalInt map(OptionalInt optional, IntUnaryOperator operator) { return optional.isPresent() ? OptionalInt.of(operator.applyAsInt(optional.getAsInt())) : optional; } + /** + * Map an {@code optional} int under an optional-integer-valued unary {@code function}. + * + * @param optional + * an optional integer value + * @param function + * a function to apply + * @return the optional {@code function} result + */ + public static OptionalInt flatMap(OptionalInt optional, IntFunction function) { + return optional.isPresent() ? function.apply(optional.getAsInt()) : optional; + } + /** * Map an optional integer value to an object. * @@ -250,4 +264,23 @@ public static boolean lessThan(OptionalInt a, OptionalInt b) { public static OptionalInt filter(OptionalInt value, IntPredicate predicate) { return (value.isPresent() && predicate.test(value.getAsInt())) ? value : OptionalInt.empty(); } + + /** + * An analogue of the {@link Optional#ifPresent(Consumer)} API that includes an {@code else} clause. + * + * @param optional + * the optional value to process + * @param ifPresent + * the present ({@code then}) case + * @param orElse + * the absent ({@code else}) case + */ + public static void ifPresentElse(Optional optional, Consumer ifPresent, + Runnable orElse) { + if (optional.isPresent()) { + ifPresent.accept(optional.get()); + } else { + orElse.run(); + } + } } 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 index 3e4de3a4..c0e0713f 100644 --- 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 @@ -19,6 +19,7 @@ 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.model.spi.ViewTypes.LIFELINE_BODY; 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; @@ -30,6 +31,7 @@ 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.AutoFixture.VisualID; 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; @@ -100,12 +102,14 @@ public static class Basic extends AbstractGraphicalEditPolicyUITest { private Lifeline l1; @AutoFixture + @VisualID(LIFELINE_BODY) private EditPart l1EP; @AutoFixture("L2") private Lifeline l2; @AutoFixture + @VisualID(LIFELINE_BODY) private EditPart l2EP; @AutoFixture diff --git a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/NestedExecutionMoveUITest.java b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/NestedExecutionMoveUITest.java new file mode 100644 index 00000000..4fcf96cd --- /dev/null +++ b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/NestedExecutionMoveUITest.java @@ -0,0 +1,186 @@ +/***************************************************************************** + * 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.uml.diagram.sequence.runtime.tests.matchers.GEFMatchers.is; +import static org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.matchers.GEFMatchers.EditParts.isAt; +import static org.eclipse.papyrus.uml.interaction.model.spi.ViewTypes.LIFELINE_BODY; +import static org.eclipse.papyrus.uml.interaction.tests.matchers.NumberMatchers.gt; +import static org.hamcrest.CoreMatchers.sameInstance; +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.emf.ecore.EObject; +import org.eclipse.gef.EditPart; +import org.eclipse.papyrus.infra.gmfdiag.common.utils.DiagramEditPartsUtil; +import org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.providers.SequenceElementTypes; +import org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.rules.AutoFixture; +import org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.rules.AutoFixture.VisualID; +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.junit.Rule; +import org.junit.Test; + +/** + * Integration tests for the moving of nested executions to other lifelines and around the same lifeline. + */ +@ModelResource("nested-executions.di") +@Maximized +public class NestedExecutionMoveUITest extends AbstractGraphicalEditPolicyUITest { + + @Rule + public final AutoFixtureRule autoFixtures = new AutoFixtureRule(this); + + @AutoFixture("Lifeline2") + @VisualID(LIFELINE_BODY) + private EditPart lifeline2; + + @AutoFixture("Lifeline3") + @VisualID(LIFELINE_BODY) + private EditPart lifeline3; + + @AutoFixture + private Rectangle lifeline3Geom; + + @AutoFixture + private EditPart request; + + @AutoFixture + private PointList requestGeom; + + @AutoFixture + private EditPart exec1; + + @AutoFixture + private Rectangle exec1Geom; + + @AutoFixture + private EditPart exec2; + + @AutoFixture + private Rectangle exec2Geom; + + @AutoFixture + private EditPart exec3; + + @AutoFixture + private Rectangle exec3Geom; + + @AutoFixture + private EditPart exec4; + + @AutoFixture + private Rectangle exec4Geom; + + /** + * Initializes me. + */ + public NestedExecutionMoveUITest() { + super(); + } + + @Test + public void moveExecutionToNested() { + Point grabAt = exec4Geom.getCenter(); + Point dropAt = exec3Geom.getLeft().getTranslated(-EXEC_WIDTH, 0); + + editor.select(grabAt); + editor.with(editor.allowSemanticReordering(), () -> editor.drag(grabAt, dropAt)); + + autoFixtures.refresh(); + + assertThat("Wrong parent", exec4.getParent(), sameInstance(exec3)); + assertThat(exec4, isAt(is(exec3Geom.x() + EXEC_WIDTH / 2), gt(exec3Geom.y()))); + } + + @Test + public void retargetRequest() { + Point grabAt = requestGeom.getLastPoint(); + Point dropAt = new Point(lifeline3Geom.getCenter().x(), grabAt.y()); + + editor.select(requestGeom.getMidpoint()); + editor.with(editor.allowSemanticReordering(), () -> editor.drag(grabAt, dropAt)); + + autoFixtures.refresh(); + + assertThat("Wrong parent", exec3.getParent(), sameInstance(exec2)); + assertThat("Wrong parent", exec2.getParent(), sameInstance(exec1)); + assertThat("Wrong parent", exec1.getParent(), sameInstance(lifeline3)); + + assertThat("Wrong nesting", exec3, isAt(gt(exec2Geom.x()), gt(exec2Geom.y()))); + assertThat("Wrong nesting", exec2, isAt(gt(exec1Geom.x()), gt(exec1Geom.y()))); + } + + @Test + public void moveNestedExecutionToOtherLifeline() { + int previousExec3Y = exec3Geom.y(); + Point grabAt = exec3Geom.getCenter(); + Point dropAt = new Point(lifeline3Geom.getCenter().x(), grabAt.y()); + + editor.with(editor.allowSemanticReordering(), () -> editor.drag(grabAt, dropAt)); + + autoFixtures.refresh(); + + assertThat("Wrong parent", exec3.getParent(), sameInstance(lifeline3)); + assertThat("Wrong nesting", exec3, + isAt(is(lifeline3Geom.getCenter().x() - (EXEC_WIDTH / 2)), is(previousExec3Y))); + } + + @Test + public void moveNestedExecutionToSpanOtherExecution() { + Point previousExec4Location = exec4Geom.getTopLeft(); + Point grabAt = exec3Geom.getCenter(); + Point dropAt = exec4Geom.getBottom(); // Optimal placement for the ratio of their sizes + + editor.with(editor.allowSemanticReordering(), () -> editor.moveSelection(grabAt, dropAt)); + + autoFixtures.refresh(); + + assertThat("Wrong parent", exec3.getParent(), sameInstance(lifeline2)); + assertThat("Wrong parent", exec4.getParent(), sameInstance(exec3)); + assertThat("Wrong nesting", exec4, + isAt(gt(previousExec4Location.x()), is(previousExec4Location.y()))); + } + + @Test + public void moveNestedExecutionToSpanExecutionOnOtherLifeline() { + // Create a new execution on lifeline3 vertically in the middle-ish of an existing execution + // on lifeline3 + Point newExecAt = new Point(lifeline3Geom.getCenter().x(), exec3Geom.getCenter().y()); + EditPart newExecEP = createShape(SequenceElementTypes.Behavior_Execution_Shape, newExecAt, null); + ExecutionSpecification newExec = (ExecutionSpecification)newExecEP.getAdapter(EObject.class); + + autoFixtures.refresh(); // Account for nudging + Point previousNewExecLocation = getBounds(newExecEP).getTopLeft(); + + // Grab the middle exec of the existing stack near the bottom where the most nested one isn't + Point grabAt = exec2Geom.getBottom().getTranslated(0, -10); + // And drop it onto lifeline3 over top of the new exec + Point dropAt = new Point(lifeline3Geom.getCenter().x(), grabAt.y()); + + editor.with(editor.allowSemanticReordering(), () -> editor.moveSelection(grabAt, dropAt)); + + // The edit-parts are re-created by the notation changes + autoFixtures.refresh(); + newExecEP = DiagramEditPartsUtil.getChildByEObject(newExec, editor.getDiagramEditPart(), false); + + assertThat("Wrong parent", newExecEP.getParent(), sameInstance(exec3)); + assertThat("Wrong nesting", newExecEP, + isAt(gt(previousNewExecLocation.x()), is(previousNewExecLocation.y()))); + } + +} diff --git a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/ShrinkExpandExecutionUITest.java b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/ShrinkExpandExecutionUITest.java index 7a873c30..79bc5936 100644 --- a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/ShrinkExpandExecutionUITest.java +++ b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/ShrinkExpandExecutionUITest.java @@ -13,6 +13,8 @@ package org.eclipse.papyrus.uml.diagram.sequence.runtime.internal.edit.policies.tests; 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.isBounded; import static org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.matchers.GEFMatchers.EditParts.runs; import static org.hamcrest.MatcherAssert.assertThat; @@ -21,11 +23,14 @@ 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.internal.providers.SequenceElementTypes; 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.junit.Before; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; @@ -40,47 +45,71 @@ public class ShrinkExpandExecutionUITest extends AbstractGraphicalEditPolicyUITest { /** Reply messages don't end up exactly where the resize handle is. */ + @SuppressWarnings("hiding") @ClassRule public static final TestRule TOLERANCE = GEFMatchers.defaultTolerance(1); + @Rule + public final TestRule autoFixtures = new AutoFixtureRule(this); + + @AutoFixture private EditPart sync1; + @AutoFixture private PointList sync1Geom; + @AutoFixture private EditPart async1; + @AutoFixture private PointList async1Geom; + @AutoFixture private EditPart sync2; + @AutoFixture private PointList sync2Geom; + @AutoFixture private EditPart reply2; + @AutoFixture private PointList reply2Geom; + @AutoFixture private EditPart async2; + @AutoFixture private PointList async2Geom; + @AutoFixture private EditPart reply1; + @AutoFixture private PointList reply1Geom; + @AutoFixture private EditPart async3; + @AutoFixture private PointList async3Geom; + @AutoFixture private EditPart async4; + @AutoFixture private PointList async4Geom; + @AutoFixture private EditPart exec1; + @AutoFixture private Rectangle exec1Geom; + @AutoFixture private EditPart exec2; + @AutoFixture private Rectangle exec2Geom; /** @@ -245,36 +274,35 @@ public void expandFromBottom() { runs(isPoint(async2Geom.getFirstPoint()), isPoint(async2Geom.getLastPoint()))); } - // - // Test framework - // - - @Before - public void findFixtures() { - sync1 = find("sync1", true); - sync1Geom = getPoints(sync1); - async1 = find("async1", true); - async1Geom = getPoints(async1); - sync2 = find("sync2", true); - sync2Geom = getPoints(sync2); - reply2 = find("reply2", true); - reply2Geom = getPoints(reply2); - async2 = find("async2", true); - async2Geom = getPoints(async2); - reply1 = find("reply1", true); - reply1Geom = getPoints(reply1); - async3 = find("async3", true); - async3Geom = getPoints(async3); - async4 = find("async4", true); - async4Geom = getPoints(async4); - - exec1 = find("exec1", false); - exec1Geom = getBounds(exec1); - exec2 = find("exec2", false); - exec2Geom = getBounds(exec2); + @Test + public void attemptExpandToHalfCoverExec() { + Point where = exec2Geom.getBottom().getTranslated(0, 15); + EditPart newExec = createShape(SequenceElementTypes.Behavior_Execution_Shape, where, null); + + editor.select(exec2Geom.getCenter()); + Point grabAt = getResizeHandleGrabPoint(exec2, PositionConstants.SOUTH); + Point dropAt = getBounds(newExec).getCenter(); + + // Even allow-reordering isn't permitted + editor.with(editor.allowSemanticReordering(), () -> editor.drag(grabAt, dropAt)); + + assertThat("Execution was resized", exec2, isBounded(isRect(exec2Geom))); } - protected EditPart find(String name, boolean isMessage) { - return findEditPart("lightweight::DoIt::" + name, isMessage); + @Test + public void attemptShrinkToHalfCoverExec() { + Point where = exec2Geom.getCenter().getTranslated(0, -10); + createShape(SequenceElementTypes.Behavior_Execution_Shape, where, null); + + exec2Geom = getBounds(exec2); // Refresh geometry + editor.select(exec2Geom.getBottom().getTranslated(0, -5)); + Point grabAt = getResizeHandleGrabPoint(exec2, PositionConstants.SOUTH); + Point dropAt = exec2Geom.getCenter().getTranslated(0, -10); + + // Even allow-reordering isn't permitted + editor.with(editor.allowSemanticReordering(), () -> editor.drag(grabAt, dropAt)); + + assertThat("Execution was resized", exec2, isBounded(isRect(exec2Geom))); } + } diff --git a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/nested-executions.di b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/nested-executions.di new file mode 100644 index 00000000..8c549eec --- /dev/null +++ b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/nested-executions.di @@ -0,0 +1,2 @@ + + diff --git a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/nested-executions.notation b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/nested-executions.notation new file mode 100644 index 00000000..650f6dd1 --- /dev/null +++ b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/nested-executions.notation @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/nested-executions.uml b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/nested-executions.uml new file mode 100644 index 00000000..5316c75c --- /dev/null +++ b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/internal/edit/policies/tests/nested-executions.uml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/AutoFixture.java b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/AutoFixture.java index 25bcf02a..5195f3e8 100644 --- a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/AutoFixture.java +++ b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/AutoFixture.java @@ -18,6 +18,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import org.eclipse.gef.EditPart; + /** * Annotation for fields that represent fixtures in the contextual model or editor, usually an * {@link EditorFixture}. @@ -27,4 +29,19 @@ public @interface AutoFixture { /** An optional element name to search for, which may be qualified or not. */ String value() default ""; + + // + // Nested types + // + + /** + * Annotation for {@link AutoFixture @AutoFixture} fields of {@link EditPart} kind to specify the visual + * ID of a child of the top edit-part to resolve as the fixture. + */ + @Retention(RUNTIME) + @Target(FIELD) + public @interface VisualID { + /** The edit-part visual ID to resolve. */ + String value(); + } } 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 index ca9ff81a..04702a15 100644 --- 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 @@ -21,17 +21,25 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; import java.util.stream.Stream; +import org.eclipse.draw2d.Connection; +import org.eclipse.draw2d.geometry.Translatable; import org.eclipse.emf.common.util.TreeIterator; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.gef.ConnectionEditPart; import org.eclipse.gef.EditPart; +import org.eclipse.gef.GraphicalEditPart; import org.eclipse.papyrus.infra.gmfdiag.common.utils.DiagramEditPartsUtil; +import org.eclipse.papyrus.infra.gmfdiag.common.utils.EditPartUtils; +import org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.rules.AutoFixture.VisualID; import org.eclipse.papyrus.uml.interaction.tests.rules.ModelFixture; import org.eclipse.uml2.uml.ActivityEdge; import org.eclipse.uml2.uml.AssociationClass; @@ -51,7 +59,9 @@ */ public class AutoFixtureRule implements TestRule { - private static final Pattern EDIT_PART_FIXTURE = Pattern.compile(".*(?=EP|EditPart)"); + private static final Pattern EDIT_PART_FIXTURE = Pattern.compile("^.*(?=EP|EditPart)"); + + private static final Pattern GEOMETRY_FIXTURE = Pattern.compile("^.*(?=Geom\\w*)"); private final Object test; @@ -68,7 +78,8 @@ public AutoFixtureRule(Object testOrTestClass) { List, Object>> resolvers = Arrays.asList( this::resolveQualifiedName, // this::resolveSimpleName, // - this::resolveEditPart); + this::resolveEditPart, // + this::resolveGeometry); resolver = (model, spec) -> resolvers.stream().map(r -> r.apply(model, spec)).filter(Objects::nonNull) .findFirst().orElse(null); } @@ -94,6 +105,25 @@ public void evaluate() throws Throwable { }; } + public void refresh() { + ModelFixture model = getField(ModelFixture.class).get(); + + getAutoFixtures().forEach(fixture -> { + Object resolved = null; + try { + resolved = resolver.apply(model, fixture); + } catch (AssertionError e) { + // Just set it null + } + + if (resolved == null) { + fixture.clear(); + } else { + fixture.set(resolved); + } + }); + } + private Stream getFields() { return getFields(test); } @@ -119,12 +149,13 @@ private FieldAccess getField(Class type) { return new FieldAccess<>(type, test); } - private FieldAccess getField(String name, Class type) { - return new FieldAccess<>(name, type, test); + private FieldAccess findField(Pattern regex, Class type) { + return getFields().filter(f -> type.isAssignableFrom(f.getType())) + .filter(f -> regex.matcher(f.getName()).find()).map(f -> new FieldAccess<>(f, type, test)) + .findFirst().orElse(null); } 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)); } @@ -134,9 +165,15 @@ private Stream> getAutoFixtures() { // private Object resolveQualifiedName(ModelFixture model, FieldAccess fixture) { - Object result = null; + return fixture.isAssignableTo(NamedElement.class) // + ? getElementByQName(model, fixture.getFixtureName()) + : null; + } + + private NamedElement getElementByQName(ModelFixture model, String name) { + NamedElement result = null; - String[] parts = fixture.getFixtureName().split("::"); + String[] parts = name.split("::"); if ((parts.length > 0) && parts[0].equals(model.getModel().getName())) { NamedElement next = model.getModel(); @@ -151,23 +188,29 @@ private Object resolveQualifiedName(ModelFixture model, FieldAccess fixture) result = next; } - return fixture.asAssignable(result); + return result; } private Object resolveSimpleName(ModelFixture model, FieldAccess fixture) { - Object result = null; + return fixture.isAssignableTo(NamedElement.class) + ? getElement(model, fixture.getFixtureName(), fixture::isAssignableFrom) + : null; + } + + private NamedElement getElement(ModelFixture model, String name, Predicate filter) { + NamedElement result = null; - String name = fixture.getFixtureName(); - if (!name.contains("::")) { + if (name.contains("::")) { + result = Optional.ofNullable(getElementByQName(model, name)).filter(filter).orElse(null); + } else { 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; + NamedElement element = (NamedElement)next; + if (name.equals(getValidJavaIdentifier(element.getName())) && filter.test(element)) { + result = element; } } else { iter.prune(); @@ -181,15 +224,48 @@ private Object resolveSimpleName(ModelFixture model, FieldAccess fixture) { private Object resolveEditPart(ModelFixture model, FieldAccess fixture) { Object result = null; - if ((model instanceof EditorFixture) && fixture.isAssignable(EditPart.class)) { + if ((model instanceof EditorFixture) && fixture.isAssignableTo(EditPart.class)) { EditorFixture editor = (EditorFixture)model; - FieldAccess element = getField(fixture.getFixtureName(EDIT_PART_FIXTURE), + String fixtureName = fixture.getFixtureName(EDIT_PART_FIXTURE); + Supplier element = findField(Pattern.compile("^" + Pattern.quote(fixtureName)), EObject.class); - if (element != null) { - EObject fixtureElement = element.get(); - result = DiagramEditPartsUtil.getChildByEObject(fixtureElement, editor.getDiagramEditPart(), - isEdge(fixtureElement)); + if (element == null) { + element = () -> getElement(model, fixtureName, __ -> true); + } + + EObject fixtureElement = element.get(); + result = DiagramEditPartsUtil.getChildByEObject(fixtureElement, editor.getDiagramEditPart(), + isEdge(fixtureElement)); + + if ((result != null) && fixture.field.isAnnotationPresent(VisualID.class)) { + String visualID = fixture.field.getAnnotation(VisualID.class).value(); + // Prefer the child + result = EditPartUtils.findFirstChildEditPartWithId((EditPart)result, visualID); + } + } + + return result; + } + + private Object resolveGeometry(ModelFixture model, FieldAccess fixture) { + Object result = null; + + if ((model instanceof EditorFixture) && fixture.isAssignableTo(Translatable.class)) { + Pattern editPartPattern = Pattern.compile( + "^" + Pattern.quote(fixture.getFixtureName(GEOMETRY_FIXTURE)) + "(?=EP|EditPart)?"); + FieldAccess editPart = findField(editPartPattern, EditPart.class); + if (editPart != null) { + EditPart ep = editPart.get(); + Translatable translatable = (ep instanceof ConnectionEditPart) + ? ((Connection)((ConnectionEditPart)ep).getFigure()).getPoints().getCopy() + : ((GraphicalEditPart)ep).getFigure().getBounds().getCopy(); + + if (translatable != null) { + ((GraphicalEditPart)ep).getFigure().getParent().translateToAbsolute(translatable); + } + + result = translatable; } } @@ -235,7 +311,7 @@ public Boolean defaultCase(EObject object) { // Nested types // - private static final class FieldAccess { + private static final class FieldAccess implements Supplier { private final Field field; private final Object owner; @@ -258,22 +334,6 @@ private static final class FieldAccess { } } - 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(); @@ -320,7 +380,7 @@ AutoFixture getAutoFixture() { return field.getAnnotation(AutoFixture.class); } - boolean isAssignable(Object object) { + boolean isAssignableFrom(Object object) { Class objectType; if (object == null) { @@ -334,11 +394,12 @@ boolean isAssignable(Object object) { return field.getType().isAssignableFrom(objectType); } - T asAssignable(Object object) { - return isAssignable(object) ? type.cast(object) : null; + boolean isAssignableTo(Class objectType) { + return objectType.isAssignableFrom(field.getType()); } - T get() { + @Override + public T get() { try { return type.cast(field.get(owner)); } catch (Exception e) { diff --git a/tests/org.eclipse.papyrus.uml.interaction.model.tests/src/org/eclipse/papyrus/uml/interaction/model/util/tests/LogicalModelPredicatesTest.java b/tests/org.eclipse.papyrus.uml.interaction.model.tests/src/org/eclipse/papyrus/uml/interaction/model/util/tests/LogicalModelPredicatesTest.java index 79b4d61a..98b13dc9 100644 --- a/tests/org.eclipse.papyrus.uml.interaction.model.tests/src/org/eclipse/papyrus/uml/interaction/model/util/tests/LogicalModelPredicatesTest.java +++ b/tests/org.eclipse.papyrus.uml.interaction.model.tests/src/org/eclipse/papyrus/uml/interaction/model/util/tests/LogicalModelPredicatesTest.java @@ -14,7 +14,10 @@ import static org.eclipse.papyrus.uml.interaction.internal.model.SequenceDiagramPackage.Literals.MELEMENT__NAME; import static org.eclipse.papyrus.uml.interaction.model.util.LogicalModelPredicates.above; +import static org.eclipse.papyrus.uml.interaction.model.util.LogicalModelPredicates.alwaysFalse; +import static org.eclipse.papyrus.uml.interaction.model.util.LogicalModelPredicates.alwaysTrue; import static org.eclipse.papyrus.uml.interaction.model.util.LogicalModelPredicates.below; +import static org.eclipse.papyrus.uml.interaction.model.util.LogicalModelPredicates.equalTo; import static org.eclipse.papyrus.uml.interaction.model.util.LogicalModelPredicates.where; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -23,6 +26,7 @@ import org.eclipse.papyrus.uml.interaction.model.tests.ModelEditFixture; import org.eclipse.papyrus.uml.interaction.model.util.LogicalModelPredicates; import org.eclipse.papyrus.uml.interaction.tests.rules.ModelResource; +import org.eclipse.uml2.uml.Element; import org.junit.Rule; import org.junit.Test; @@ -74,4 +78,25 @@ public void where_EStructuralFeature_Object() { assertThat(where(MELEMENT__NAME, "request-recv").test(requestRecv), is(true)); assertThat(where(MELEMENT__NAME, "request-recv").test(replySend), is(false)); } + + @Test + public void alwaysTrue_() { + assertThat(alwaysTrue().test(model.getMInteraction()), is(true)); + assertThat(alwaysTrue().test(null), is(true)); + } + + @Test + public void alwaysFalse_() { + assertThat(alwaysFalse().test(model.getMInteraction()), is(false)); + assertThat(alwaysFalse().test(null), is(false)); + } + + @Test + public void equalTo_MElement() { + MElement m = model.getMInteraction(); + MElement l = model.getMInteraction().getLifelines().get(0); + + assertThat(equalTo(m).test(m), is(true)); + assertThat(equalTo(m).test(l), is(false)); + } } From 341b02e0304c585527123b06fc6f6918c3bb1a96 Mon Sep 17 00:00:00 2001 From: "Christian W. Damus" Date: Wed, 28 Nov 2018 11:34:27 -0500 Subject: [PATCH 3/3] Fix #411 Updating nested on nesting resize Address the case identified in code review of a nesting execution being resized leaving a formerly nested execution hanging in space. Includes update to the auto-fixture rule to support optional (dynamic) fixtures to simplify again the specification of tests. Signed-off-by: Christian W. Damus --- .../model/commands/PendingNestedData.java | 11 ++- .../commands/PendingVerticalExtentData.java | 15 +++ .../model/commands/SetOwnerCommand.java | 9 ++ .../model/util/LogicalModelPredicates.java | 15 +++ ...tionSpecificationDragEditPolicyUITest.java | 96 +++++++++++++++++++ .../runtime/tests/rules/AutoFixture.java | 3 + .../runtime/tests/rules/AutoFixtureRule.java | 61 +++++++----- 7 files changed, 186 insertions(+), 24 deletions(-) diff --git a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingNestedData.java b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingNestedData.java index 3998a2f9..f42c9ec0 100644 --- a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingNestedData.java +++ b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/internal/model/commands/PendingNestedData.java @@ -12,6 +12,7 @@ package org.eclipse.papyrus.uml.interaction.internal.model.commands; +import java.lang.reflect.Proxy; import java.util.List; import java.util.Optional; @@ -23,6 +24,14 @@ * @see DependencyContext */ public final class PendingNestedData extends PendingContainmentChangeData { + /** + * A token representing a lifeline as the top level of nested, the non-existent parent of a root + * execution. + */ + public static final MExecution NO_EXECUTION = (MExecution)Proxy.newProxyInstance( + PendingNestedData.class.getClassLoader(), new Class[] {MExecution.class }, + (proxy, method, args) -> null); + private PendingNestedData(MExecution nesting, MExecution nested) { super(nesting, nested); } @@ -72,7 +81,7 @@ public static Optional getPendingNestingExecution(MExecution nested) * an execution that is getting the new {@code nesting} */ static void setPendingNested(MExecution nesting, MExecution pendingNested) { - if (!nesting.getNestedExecutions().stream() + if ((nesting == NO_EXECUTION) || !nesting.getNestedExecutions().stream() .anyMatch(n -> n.getElement() == pendingNested.getElement())) { PendingContainmentChangeData.setPendingChild(PendingNestedData.class, nesting, pendingNested, 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 c6fe8e00..266dcf1f 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 @@ -25,6 +25,7 @@ import org.eclipse.papyrus.uml.interaction.model.MExecution; import org.eclipse.papyrus.uml.interaction.model.MLifeline; import org.eclipse.papyrus.uml.interaction.model.MOccurrence; +import org.eclipse.papyrus.uml.interaction.model.util.LogicalModelPredicates; import org.eclipse.papyrus.uml.interaction.model.util.Optionals; import org.eclipse.uml2.uml.Element; @@ -196,4 +197,18 @@ public static Stream> affectedOccurrences(MExecut return Stream.concat(Stream.concat(bracketing, spannedNow), futureSpanned).distinct(); } + + /** + * Obtain a predicate that queries whether an element is spanned by the given potentially {@code spanning} + * element at the time of the query's evaluation, based on future knowledge of vertical extents. + * + * @param spanning + * an element that may or may not span other elements + * @return the is-spanned-by-baesd-on-future-vertical-extends predicate + */ + public static Predicate> spannedBy(MElement spanning) { + return LogicalModelPredicates.verticallyBetween( // + () -> getPendingTop(spanning).orElse(Integer.MAX_VALUE), // + () -> getPendingBottom(spanning).orElse(Integer.MIN_VALUE)); + } } 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 9bab0e54..e5818e88 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 @@ -13,6 +13,7 @@ package org.eclipse.papyrus.uml.interaction.internal.model.commands; import static java.util.Collections.singletonList; +import static org.eclipse.papyrus.uml.interaction.internal.model.commands.PendingNestedData.setPendingNested; import static org.eclipse.papyrus.uml.interaction.internal.model.commands.PendingVerticalExtentData.affectedOccurrences; import static org.eclipse.papyrus.uml.interaction.model.util.Executions.executionShapeAt; import static org.eclipse.papyrus.uml.interaction.model.util.LogicalModelPredicates.equalTo; @@ -20,6 +21,7 @@ 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.Objects; import java.util.Optional; import java.util.OptionalInt; import java.util.function.UnaryOperator; @@ -185,6 +187,13 @@ protected Optional dependencies(MExecution execution, MLifeline lifelin // that *will be* spanned do not; they only must be reattached per the step below result = execution.getOccurrences().stream() .map(occ -> occ.setCovered(lifeline, topMapping.apply(occ.getTop()))).reduce(chaining()); + } else if (isChangingPosition() && !isChangingOwner()) { + // We are reshaping it. Refresh nested executions, if necessary + result = execution.getNestedExecutions().stream() + .filter(PendingVerticalExtentData.spannedBy(execution).negate()) + .peek(nested -> setPendingNested(PendingNestedData.NO_EXECUTION, nested)) + .map(nested -> nested.setOwner(lifeline, nested.getTop(), nested.getBottom())) + .filter(Objects::nonNull).reduce(chaining()); } // Note that nested executions will be handled implicitly by either their start or finish. diff --git a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/model/util/LogicalModelPredicates.java b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/model/util/LogicalModelPredicates.java index 996cc6d3..88ca6faf 100644 --- a/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/model/util/LogicalModelPredicates.java +++ b/plugins/org.eclipse.papyrus.uml.interaction.model/src/org/eclipse/papyrus/uml/interaction/model/util/LogicalModelPredicates.java @@ -14,6 +14,7 @@ import java.util.Objects; import java.util.OptionalInt; +import java.util.function.IntSupplier; import java.util.function.Predicate; import org.eclipse.emf.ecore.EObject; @@ -51,6 +52,10 @@ public static Predicate> above(int y) { return self -> self.getTop().orElse(Integer.MAX_VALUE) < y; } + public static Predicate> above(IntSupplier y) { + return self -> self.getTop().orElse(Integer.MAX_VALUE) < y.getAsInt(); + } + public static Predicate> above(MElement other) { return above(other.getTop().orElse(Integer.MIN_VALUE)); } @@ -59,6 +64,10 @@ public static Predicate> below(int y) { return self -> self.getBottom().orElse(Integer.MIN_VALUE) > y; } + public static Predicate> below(IntSupplier y) { + return self -> self.getBottom().orElse(Integer.MIN_VALUE) > y.getAsInt(); + } + public static Predicate> below(MElement other) { return below(other.getBottom().orElse(Integer.MAX_VALUE)); } @@ -67,6 +76,12 @@ public static Predicate> verticallyBetween(int top, return above(bottom).and(below(top)); } + public static Predicate> verticallyBetween(IntSupplier top, + IntSupplier bottom) { + + return above(bottom).and(below(top)); + } + public static Predicate> where(EStructuralFeature feature, Object value) { return elem -> Objects.equals(((EObject)elem).eGet(feature), value); } 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 index c0e0713f..35f71320 100644 --- 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 @@ -19,16 +19,20 @@ 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.diagram.sequence.runtime.tests.rules.EditorFixture.at; +import static org.eclipse.papyrus.uml.diagram.sequence.runtime.tests.rules.EditorFixture.sized; import static org.eclipse.papyrus.uml.interaction.model.spi.ViewTypes.LIFELINE_BODY; 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.PositionConstants; 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.internal.providers.SequenceElementTypes; 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.AutoFixture.VisualID; @@ -38,6 +42,7 @@ import org.eclipse.uml2.uml.ExecutionSpecification; import org.eclipse.uml2.uml.Lifeline; import org.eclipse.uml2.uml.Message; +import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; @@ -447,4 +452,95 @@ public void attemptToSurroundDestruction() { } + /** + * Tests for drawing a nesting execution out from under its nested executions. + */ + @ModelResource("two-lifelines.di") + @Maximized + public static class ResizeNesting extends AbstractGraphicalEditPolicyUITest { + @Rule + public final AutoFixtureRule autoFixtures = new AutoFixtureRule(this); + + @AutoFixture("Lifeline1") + private Lifeline lifeline1; + + @AutoFixture + private EditPart lifeline1EP; + + @AutoFixture + private Rectangle lifelineGeom; + + @AutoFixture(value = "Execution1", optional = true) + private EditPart nestingEP; + + @AutoFixture(optional = true) + private Rectangle nestingGeom; + + @AutoFixture(value = "Execution2", optional = true) + private EditPart nestedEP; + + @AutoFixture(optional = true) + private Rectangle nestedGeom; + + /** + * Initializes me. + */ + public ResizeNesting() { + super(); + } + + @Test + public void drawUpBottomToExposeNested() { + // First, select the nesting execution to activate selection handles + editor.select(nestingGeom.getBottom().getTranslated(0, -20)); + + Point grabAt = getResizeHandleGrabPoint(nestingEP, PositionConstants.SOUTH); + Point dropAt = nestedGeom.getTop().getTranslated(0, -25); + + // Now, draw up the bottom from under the nested execution + editor.drag(grabAt, dropAt); + Rectangle expectedGeom = nestedGeom.getTranslated(-EXEC_WIDTH / 2, 0); + + // The operation rebuilds edit-parts + autoFixtures.refresh(); + + assertThat("Formerly nested execution misaligned", nestedEP, isBounded(isRect(expectedGeom))); + } + + @Test + public void drawDownTopToExposeNested() { + // First, select the nesting execution to activate selection handles + editor.select(nestingGeom.getTop().getTranslated(0, 20)); + + Point grabAt = getResizeHandleGrabPoint(nestingEP, PositionConstants.NORTH); + Point dropAt = nestedGeom.getBottom().getTranslated(0, 25); + + // Now, draw down the top from under the nested execution + editor.drag(grabAt, dropAt); + Rectangle expectedGeom = nestedGeom.getTranslated(-EXEC_WIDTH / 2, 0); + + // The operation rebuilds edit-parts + autoFixtures.refresh(); + + assertThat("Formerly nested execution misaligned", nestedEP, isBounded(isRect(expectedGeom))); + } + + // + // Test fixtures + // + + @Before + public void createExecutions() { + int x = lifelineGeom.getCenter().x(); + + nestingEP = editor.createShape(SequenceElementTypes.Behavior_Execution_Shape, at(x, 150), + sized(0, 200)); + nestedEP = editor.createShape(SequenceElementTypes.Behavior_Execution_Shape, at(x, 300), + null /* default size */); + + nestingGeom = getBounds(nestingEP); + nestedGeom = getBounds(nestedEP); + } + } + } diff --git a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/AutoFixture.java b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/AutoFixture.java index 5195f3e8..084d8a38 100644 --- a/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/AutoFixture.java +++ b/tests/org.eclipse.papyrus.uml.diagram.sequence.runtime.tests/src/org/eclipse/papyrus/uml/diagram/sequence/runtime/tests/rules/AutoFixture.java @@ -30,6 +30,9 @@ /** An optional element name to search for, which may be qualified or not. */ String value() default ""; + /** Whether the fixture is allowed not to exist (to be resolvable) in the diagram. */ + boolean optional() default false; + // // Nested types // 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 index 04702a15..407b2184 100644 --- 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 @@ -151,13 +151,14 @@ private FieldAccess getField(Class type) { private FieldAccess findField(Pattern regex, Class type) { return getFields().filter(f -> type.isAssignableFrom(f.getType())) - .filter(f -> regex.matcher(f.getName()).find()).map(f -> new FieldAccess<>(f, type, test)) - .findFirst().orElse(null); + .filter(f -> regex.matcher(f.getName()).find()) + // FieldAccess found this way doesn't need to be set, so optionality is fine + .map(f -> new FieldAccess<>(f, type, test, true)).findFirst().orElse(null); } private Stream> getAutoFixtures() { - return getFields().filter(f -> f.isAnnotationPresent(AutoFixture.class)) - .map(f -> new FieldAccess<>(f, Object.class, test)); + return getFields().filter(f -> f.isAnnotationPresent(AutoFixture.class)).map( + f -> new FieldAccess<>(f, Object.class, test, f.getAnnotation(AutoFixture.class).optional())); } // @@ -235,13 +236,15 @@ private Object resolveEditPart(ModelFixture model, FieldAccess fixture) { } EObject fixtureElement = element.get(); - result = DiagramEditPartsUtil.getChildByEObject(fixtureElement, editor.getDiagramEditPart(), - isEdge(fixtureElement)); - - if ((result != null) && fixture.field.isAnnotationPresent(VisualID.class)) { - String visualID = fixture.field.getAnnotation(VisualID.class).value(); - // Prefer the child - result = EditPartUtils.findFirstChildEditPartWithId((EditPart)result, visualID); + if (fixtureElement != null) { // It could be optional + result = DiagramEditPartsUtil.getChildByEObject(fixtureElement, editor.getDiagramEditPart(), + isEdge(fixtureElement)); + + if ((result != null) && fixture.field.isAnnotationPresent(VisualID.class)) { + String visualID = fixture.field.getAnnotation(VisualID.class).value(); + // Prefer the child + result = EditPartUtils.findFirstChildEditPartWithId((EditPart)result, visualID); + } } } @@ -257,15 +260,17 @@ private Object resolveGeometry(ModelFixture model, FieldAccess fixture) { FieldAccess editPart = findField(editPartPattern, EditPart.class); if (editPart != null) { EditPart ep = editPart.get(); - Translatable translatable = (ep instanceof ConnectionEditPart) - ? ((Connection)((ConnectionEditPart)ep).getFigure()).getPoints().getCopy() - : ((GraphicalEditPart)ep).getFigure().getBounds().getCopy(); + if (ep != null) { // It could be optional + Translatable translatable = (ep instanceof ConnectionEditPart) + ? ((Connection)((ConnectionEditPart)ep).getFigure()).getPoints().getCopy() + : ((GraphicalEditPart)ep).getFigure().getBounds().getCopy(); - if (translatable != null) { - ((GraphicalEditPart)ep).getFigure().getParent().translateToAbsolute(translatable); - } + if (translatable != null) { + ((GraphicalEditPart)ep).getFigure().getParent().translateToAbsolute(translatable); + } - result = translatable; + result = translatable; + } } } @@ -318,6 +323,8 @@ private static final class FieldAccess implements Supplier { private final Class type; + private final boolean optional; + FieldAccess(Class type, Object owner) { super(); @@ -327,14 +334,17 @@ private static final class FieldAccess implements Supplier { this.owner = owner; this.type = field.getType().asSubclass(type); + + // FieldAccess constructed this way is not used as a test fixture, so optionality is fine + this.optional = true; } catch (Exception e) { e.printStackTrace(); - fail(String.format("Cannnot access fixture of type %s", type.getName())); + fail(String.format("Cannot access fixture of type %s", type.getName())); throw new Error(); // Unreachable } } - FieldAccess(Field field, Class type, Object owner) { + FieldAccess(Field field, Class type, Object owner, boolean optional) { super(); try { @@ -343,9 +353,10 @@ private static final class FieldAccess implements Supplier { this.owner = owner; this.type = field.getType().asSubclass(type); + this.optional = optional; } catch (Exception e) { e.printStackTrace(); - fail(String.format("Cannnot access fixture %s", field.getName())); + fail(String.format("Cannot access fixture %s", field.getName())); throw new Error(); // Unreachable } } @@ -409,14 +420,18 @@ public T get() { } } + public boolean isRequired() { + return !optional; + } + void set(Object resolved) { - if (resolved == null) { + if ((resolved == null) && isRequired()) { fail(String.format("Could not resolve fixture %s of type %s", field.getName(), type.getName())); return; // Unreachable } - if (type.isInstance(resolved)) { + if ((resolved == null) || type.isInstance(resolved)) { try { field.set(owner, resolved); } catch (Exception e) {