Skip to content

Commit f9477f4

Browse files
committed
[2154] Handle multi-selection for Duplicate element tool
Bug: #2154 Signed-off-by: Axel RICHARD <axel.richard@obeo.fr>
1 parent a92ccdb commit f9477f4

9 files changed

Lines changed: 334 additions & 41 deletions

File tree

CHANGELOG.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ Stakeholders are by default represented with a dedicated graphical node connecte
7777
Standard libraries are now cached between tests to speed up editing context creation.
7878
This feature can be de-activated by setting the property `org.eclipse.syson.test.cacheStandardLibraries` to `false` in `application.properties`.
7979
The cache holding standard libraries can be invalidated for a specific test method or test class by using the `@InvalidateStandardLibrariesCache` annotation, ensuring the editing contexts are loaded from scratch.
80+
- https://github.com/eclipse-syson/syson/issues/2154[#2154] [diagrams] Improve the _Duplicate Element_ diagram tool to support multi-selection in standard diagrams.
8081

8182
=== New features
8283

backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/general/view/GVDuplicateNodeTest.java

Lines changed: 195 additions & 6 deletions
Large diffs are not rendered by default.

backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/testers/ToolTester.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,15 @@ public void invokeTool(String editingContextId, AtomicReference<Diagram> diagram
7777
}
7878

7979
public void invokeTool(String editingContextId, String diagramId, String diagramElementId, String toolId, List<ToolVariable> variables) {
80+
this.invokeTool(editingContextId, diagramId, List.of(diagramElementId), toolId, variables);
81+
}
82+
83+
public void invokeTool(String editingContextId, String diagramId, List<String> diagramElementIds, String toolId, List<ToolVariable> variables) {
8084
var input = new InvokeSingleClickOnDiagramElementToolInput(
8185
UUID.randomUUID(),
8286
editingContextId,
8387
diagramId,
84-
List.of(diagramElementId),
88+
diagramElementIds,
8589
toolId,
8690
0,
8791
0,

backend/application/syson-application/src/test/java/org/eclipse/syson/services/diagrams/DiagramDescriptionIdProvider.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ public String getDiagramCreationToolId(String toolName) {
5656
return creationToolId.get();
5757
}
5858

59+
public String getGroupNodeToolId(String toolName) {
60+
Optional<String> creationToolId = Optional.ofNullable(this.diagramDescription.getGroupPalette())
61+
.stream()
62+
.flatMap(groupPalette -> EMFUtils.allContainedObjectOfType(groupPalette, NodeTool.class))
63+
.filter(nodeTool -> nodeTool.getName().equals(toolName))
64+
.map(nodeTool -> UUID.nameUUIDFromBytes(EcoreUtil.getURI(nodeTool).toString().getBytes()).toString())
65+
.findFirst();
66+
assertThat(creationToolId).as(this.shouldExist("Group tool " + toolName)).isPresent();
67+
return creationToolId.get();
68+
}
69+
5970
public String getNodeToolId(String nodeDescriptionName, String toolName) {
6071
Optional<String> nodeToolId = EMFUtils.allContainedObjectOfType(this.diagramDescription, NodeDescription.class)
6172
.filter(nodeDescription -> nodeDescription.getName().equals(nodeDescriptionName))

backend/services/syson-diagram-services/src/main/java/org/eclipse/syson/diagram/services/DiagramMutationDiagramService.java

Lines changed: 77 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
import java.util.List;
1616
import java.util.Map;
1717
import java.util.Objects;
18+
import java.util.stream.IntStream;
1819

1920
import org.eclipse.sirius.components.collaborative.api.IRepresentationMetadataPersistenceService;
2021
import org.eclipse.sirius.components.collaborative.api.IRepresentationPersistenceService;
2122
import org.eclipse.sirius.components.collaborative.diagrams.DiagramContext;
2223
import org.eclipse.sirius.components.collaborative.diagrams.api.IDiagramCreationService;
2324
import org.eclipse.sirius.components.core.RepresentationMetadata;
2425
import org.eclipse.sirius.components.core.api.IEditingContext;
26+
import org.eclipse.sirius.components.core.api.IIdentityService;
2527
import org.eclipse.sirius.components.core.api.IRepresentationDescriptionSearchService;
2628
import org.eclipse.sirius.components.diagrams.Diagram;
2729
import org.eclipse.sirius.components.diagrams.Node;
@@ -56,15 +58,18 @@ public class DiagramMutationDiagramService {
5658

5759
private final ModelMutationElementService modelMutationElementService;
5860

61+
private final IIdentityService identityService;
62+
5963
public DiagramMutationDiagramService(IDiagramCreationService diagramCreationService, IRepresentationDescriptionSearchService representationDescriptionSearchService,
6064
IRepresentationMetadataPersistenceService representationMetadataPersistenceService, IRepresentationPersistenceService representationPersistenceService,
61-
DiagramMutationExposeService diagramMutationExposeService, ModelMutationElementService modelMutationElementService) {
65+
DiagramMutationExposeService diagramMutationExposeService, ModelMutationElementService modelMutationElementService, IIdentityService identityService) {
6266
this.diagramCreationService = Objects.requireNonNull(diagramCreationService);
6367
this.representationDescriptionSearchService = Objects.requireNonNull(representationDescriptionSearchService);
6468
this.representationMetadataPersistenceService = Objects.requireNonNull(representationMetadataPersistenceService);
6569
this.representationPersistenceService = Objects.requireNonNull(representationPersistenceService);
6670
this.diagramMutationExposeService = Objects.requireNonNull(diagramMutationExposeService);
6771
this.modelMutationElementService = Objects.requireNonNull(modelMutationElementService);
72+
this.identityService = Objects.requireNonNull(identityService);
6873
}
6974

7075
/**
@@ -105,21 +110,61 @@ public Element createDiagram(Element element, IEditingContext editingContext, St
105110
}
106111

107112
/**
108-
* Duplicates the given element (with its owning Relationship) and exposes the duplicated element. Note that this method will do nothing on {@link org.eclipse.syson.sysml.Relationship}.
113+
* Duplicates the given elements (with their owning Relationship) and exposes the duplicated elements. Note that
114+
* this method will do nothing on {@link org.eclipse.syson.sysml.Relationship}.
115+
*
116+
* @param elements
117+
* the elements to duplicate
118+
* @param editingContext
119+
* the {@link IEditingContext}
120+
* @param diagramContext
121+
* the {@link DiagramContext}
122+
* @param nodes
123+
* the nodes that will be duplicated
124+
* @param convertedNodes
125+
* the map of the converted nodes for this diagram
126+
* @return the duplicated elements if any
127+
*/
128+
public List<Element> duplicateElementAndExpose(List<Element> elements, IEditingContext editingContext, DiagramContext diagramContext, List<Node> nodes,
129+
Map<org.eclipse.sirius.components.view.diagram.NodeDescription, NodeDescription> convertedNodes) {
130+
return IntStream.range(0, Math.min(elements.size(), nodes.size()))
131+
.mapToObj(index -> this.duplicateElementAndExpose(elements.get(index), editingContext, diagramContext, nodes.get(index), convertedNodes))
132+
.filter(Objects::nonNull)
133+
.toList();
134+
}
135+
136+
/**
137+
* AQL-facing entry point for duplication.
138+
* <p>
139+
* Sirius invokes this service once per selected semantic receiver, while still providing the full selection in
140+
* {@code nodes}. This method therefore resolves the node corresponding to the current semantic receiver and
141+
* delegates to the list-based implementation with one semantic/node pair.
109142
*
110143
* @param element
111-
* the element to duplicate
144+
* the current semantic receiver
112145
* @param editingContext
113146
* the {@link IEditingContext}
114147
* @param diagramContext
115148
* the {@link DiagramContext}
116-
* @param node
117-
* the node that will be duplicated
149+
* @param nodes
150+
* the selected nodes
118151
* @param convertedNodes
119152
* the map of the converted nodes for this diagram
120-
* @return the duplicated element if any
153+
* @return the current semantic receiver
121154
*/
122-
public Element duplicateElementAndExpose(Element element, IEditingContext editingContext, DiagramContext diagramContext, Node node, Map<org.eclipse.sirius.components.view.diagram.NodeDescription, NodeDescription> convertedNodes) {
155+
public Element duplicateElementAndExpose(Element element, IEditingContext editingContext, DiagramContext diagramContext, List<Node> nodes,
156+
Map<org.eclipse.sirius.components.view.diagram.NodeDescription, NodeDescription> convertedNodes) {
157+
if (element != null && nodes != null) {
158+
nodes.stream()
159+
.filter(node -> Objects.equals(this.identityService.getId(element), node.getTargetObjectId()))
160+
.findFirst()
161+
.ifPresent(node -> this.duplicateElementAndExpose(List.of(element), editingContext, diagramContext, List.of(node), convertedNodes));
162+
}
163+
return element;
164+
}
165+
166+
private Element duplicateElementAndExpose(Element element, IEditingContext editingContext, DiagramContext diagramContext, Node node,
167+
Map<org.eclipse.sirius.components.view.diagram.NodeDescription, NodeDescription> convertedNodes) {
123168
OwningMembership owningMembership = element.getOwningMembership();
124169
Element result = null;
125170
if (owningMembership != null) {
@@ -134,30 +179,33 @@ public Element duplicateElementAndExpose(Element element, IEditingContext editin
134179
}
135180
}
136181

137-
if (result != null) {
138-
Node creationRequestSource = null;
139-
// The expose API requires to pass the node used for a basic creation request.
140-
// For a duplication, this node would either be the parent (bordered node) , grandparent (for list item) or the diagram
141-
Object parent = new NodeFinder(diagramContext.diagram()).getParent(node);
142-
if (parent instanceof Node parentNode) {
143-
// In case the parent node is compartment go up once again
144-
boolean isParentCompartment = convertedNodes.entrySet().stream()
145-
.filter(entry -> entry.getValue().getId().equals(parentNode.getDescriptionId()))
146-
.map(Map.Entry::getKey)
147-
.map(nodeDescription -> nodeDescription.getName().contains("Compartment"))
148-
.findFirst()
149-
.orElse(false);
150-
if (isParentCompartment) {
151-
Object grandParent = new NodeFinder(diagramContext.diagram()).getParent(parentNode);
152-
if (grandParent instanceof Node grandParentNode) {
153-
creationRequestSource = grandParentNode;
154-
}
155-
} else {
156-
creationRequestSource = parentNode;
182+
if (result != null && node != null) {
183+
this.diagramMutationExposeService.expose(result, editingContext, diagramContext, this.getCreationRequestSource(diagramContext, node, convertedNodes), convertedNodes);
184+
}
185+
return result;
186+
}
187+
188+
private Node getCreationRequestSource(DiagramContext diagramContext, Node node, Map<org.eclipse.sirius.components.view.diagram.NodeDescription, NodeDescription> convertedNodes) {
189+
Node creationRequestSource = null;
190+
// The expose API requires to pass the node used for a basic creation request.
191+
// For a duplication, this node would either be the parent (bordered node), grandparent (for list item) or the diagram.
192+
Object parent = new NodeFinder(diagramContext.diagram()).getParent(node);
193+
if (parent instanceof Node parentNode) {
194+
boolean isParentCompartment = convertedNodes.entrySet().stream()
195+
.filter(entry -> entry.getValue().getId().equals(parentNode.getDescriptionId()))
196+
.map(Map.Entry::getKey)
197+
.map(nodeDescription -> nodeDescription.getName().contains("Compartment"))
198+
.findFirst()
199+
.orElse(false);
200+
if (isParentCompartment) {
201+
Object grandParent = new NodeFinder(diagramContext.diagram()).getParent(parentNode);
202+
if (grandParent instanceof Node grandParentNode) {
203+
creationRequestSource = grandParentNode;
157204
}
205+
} else {
206+
creationRequestSource = parentNode;
158207
}
159-
this.diagramMutationExposeService.expose(result, editingContext, diagramContext, creationRequestSource, convertedNodes);
160208
}
161-
return result;
209+
return creationRequestSource;
162210
}
163211
}

backend/services/syson-diagram-services/src/main/java/org/eclipse/syson/diagram/services/aql/DiagramMutationAQLService.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
*******************************************************************************/
1313
package org.eclipse.syson.diagram.services.aql;
1414

15+
import java.util.List;
1516
import java.util.Map;
1617
import java.util.Objects;
1718

@@ -214,11 +215,11 @@ public Element dropSubjectFromDiagram(Element droppedElement, Node droppedNode,
214215
}
215216

216217
/**
217-
* {@link DiagramMutationDiagramService#duplicateElementAndExpose(Element, IEditingContext, DiagramContext, Node, Map)}.
218+
* {@link DiagramMutationDiagramService#duplicateElementAndExpose(Element, IEditingContext, DiagramContext, List, Map)}.
218219
*/
219-
public Element duplicateElementAndExpose(Element element, IEditingContext editingContext, DiagramContext diagramContext, Node node,
220+
public Element duplicateElementAndExpose(Element element, IEditingContext editingContext, DiagramContext diagramContext, List<Node> nodes,
220221
Map<org.eclipse.sirius.components.view.diagram.NodeDescription, NodeDescription> convertedNodes) {
221-
return this.diagramMutationDiagramService.duplicateElementAndExpose(element, editingContext, diagramContext, node, convertedNodes);
222+
return this.diagramMutationDiagramService.duplicateElementAndExpose(element, editingContext, diagramContext, nodes, convertedNodes);
222223
}
223224

224225
/**

backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/nodes/AbstractNodeDescriptionProvider.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import java.util.ArrayList;
1616
import java.util.List;
17+
import java.util.Map;
1718
import java.util.Objects;
1819

1920
import org.eclipse.emf.common.util.EList;
@@ -102,10 +103,29 @@ protected NodeTool getDuplicateElementAndNodeTool() {
102103
.preconditionExpression(AQLConstants.AQL + "self.oclIsKindOf(sysml::Element) and not self.oclIsKindOf(sysml::Relationship)")
103104
.body(this.viewBuilderHelper.newChangeContext()
104105
.expression(
105-
ServiceMethod.of4(DiagramMutationAQLService::duplicateElementAndExpose)
106+
ServiceMethod.of4(DiagramMutationAQLService.class, DiagramMutationAQLService::duplicateElementAndExpose, Element.class, IEditingContext.class,
107+
DiagramContext.class, List.class, Map.class)
106108
.aqlSelf(IEditingContext.EDITING_CONTEXT,
107109
DiagramContext.DIAGRAM_CONTEXT,
108-
Node.SELECTED_NODE,
110+
"Sequence{selectedNode}",
111+
ViewDiagramDescriptionConverter.CONVERTED_NODES_VARIABLE))
112+
.build())
113+
.build();
114+
}
115+
116+
protected NodeTool getDuplicateElementsAndNodesTool() {
117+
return this.diagramBuilderHelper.newNodeTool()
118+
.name("Duplicate Element")
119+
.iconURLsExpression("/images/content_copy.svg")
120+
.preconditionExpression(AQLConstants.AQL
121+
+ "selectedNodes->notEmpty() and selectedEdges->isEmpty() and self->forAll(e | e.oclIsKindOf(sysml::Element) and not e.oclIsKindOf(sysml::Relationship))")
122+
.body(this.viewBuilderHelper.newChangeContext()
123+
.expression(
124+
ServiceMethod.of4(DiagramMutationAQLService.class, DiagramMutationAQLService::duplicateElementAndExpose, Element.class, IEditingContext.class,
125+
DiagramContext.class, List.class, Map.class)
126+
.aqlSelf(IEditingContext.EDITING_CONTEXT,
127+
DiagramContext.DIAGRAM_CONTEXT,
128+
"selectedNodes",
109129
ViewDiagramDescriptionConverter.CONVERTED_NODES_VARIABLE))
110130
.build())
111131
.build();

backend/views/syson-standard-diagrams-view/src/main/java/org/eclipse/syson/standard/diagrams/view/SDVDiagramDescriptionProvider.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.eclipse.sirius.components.view.diagram.DiagramPalette;
3636
import org.eclipse.sirius.components.view.diagram.DiagramToolSection;
3737
import org.eclipse.sirius.components.view.diagram.DropNodeTool;
38+
import org.eclipse.sirius.components.view.diagram.GroupPalette;
3839
import org.eclipse.sirius.components.view.diagram.ListLayoutStrategyDescription;
3940
import org.eclipse.sirius.components.view.diagram.NodeDescription;
4041
import org.eclipse.sirius.components.view.diagram.NodeTool;
@@ -379,10 +380,27 @@ public RepresentationDescription create(IColorProvider colorProvider) {
379380

380381
var palette = this.createDiagramPalette(cache);
381382
diagramDescription.setPalette(palette);
383+
diagramDescription.setGroupPalette(this.createGroupPalette());
382384

383385
return diagramDescription;
384386
}
385387

388+
private GroupPalette createGroupPalette() {
389+
return this.diagramBuilderHelper.newGroupPalette()
390+
.quickAccessTools(this.diagramBuilderHelper.newNodeTool()
391+
.name("Duplicate Element")
392+
.iconURLsExpression("/images/content_copy.svg")
393+
.preconditionExpression("aql:selectedNodes->notEmpty() and selectedEdges->isEmpty() and self->forAll(e | e.oclIsKindOf(sysml::Element) and not e.oclIsKindOf(sysml::Relationship))")
394+
.body(this.viewBuilderHelper.newChangeContext()
395+
.expression(ServiceMethod.of4(DiagramMutationAQLService.class, DiagramMutationAQLService::duplicateElementAndExpose, org.eclipse.syson.sysml.Element.class,
396+
IEditingContext.class, DiagramContext.class, List.class, Map.class)
397+
.aqlSelf(IEditingContext.EDITING_CONTEXT, DiagramContext.DIAGRAM_CONTEXT, "selectedNodes",
398+
ViewDiagramDescriptionConverter.CONVERTED_NODES_VARIABLE))
399+
.build())
400+
.build())
401+
.build();
402+
}
403+
386404
protected IDescriptionNameGenerator getDescriptionNameGenerator() {
387405
return this.descriptionNameGenerator;
388406
}

doc/content/modules/user-manual/pages/release-notes/2026.5.0.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ image::release-notes-stakeholder-node.png[Default representation of Stakeholder
3737
** Improve the tool to create a `Subject` graphical node by making specialization selection optional.
3838
** Improve the tool to create a `FlowUsage` from a `ConnectionUsage` by making the payload selection optional.
3939
** Merge the two objective creation tools into a single tool by leveraging the updated selection dialog which make the specialization selection optional.
40+
** Improve the _Duplicate Element_ tool to support multi-selection in standard diagrams.
4041

4142
* In the _Explorer_ view:
4243

0 commit comments

Comments
 (0)