Skip to content

Commit c3cd92c

Browse files
mcharfadisbegaudeau
authored andcommitted
[5300] Add support for undo redo node layout data
Bug: #5300 Signed-off-by: Michaël Charfadi <michael.charfadi@obeosoft.com>
1 parent d737ce9 commit c3cd92c

File tree

14 files changed

+556
-20
lines changed

14 files changed

+556
-20
lines changed

CHANGELOG.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ In conjonction of the `workbenchViewContributionExtensionPoint` frontend extensi
209209
- https://github.com/eclipse-sirius/sirius-web/issues/5585[#5585] [sirius-web] Replace use of foreign keys between different bounded contexts with proper events
210210
- https://github.com/eclipse-sirius/sirius-web/issues/5958[#5958] [diagram] Allow the execution of adjust size tool on a multi selection
211211
- https://github.com/eclipse-sirius/sirius-web/issues/5937[#5937] [diagram] Add support for bending points for oblique edges
212+
- https://github.com/eclipse-sirius/sirius-web/issues/5300[#5300] [diagram] Add undo redo for node layout
212213

213214

214215
=== Improvements

packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/DiagramCreationService.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.eclipse.sirius.components.diagrams.components.DiagramComponentProps.Builder;
4040
import org.eclipse.sirius.components.diagrams.description.DiagramDescription;
4141
import org.eclipse.sirius.components.diagrams.events.IDiagramEvent;
42+
import org.eclipse.sirius.components.diagrams.events.undoredo.DiagramNodeLayoutEvent;
4243
import org.eclipse.sirius.components.diagrams.layoutdata.DiagramLayoutData;
4344
import org.eclipse.sirius.components.diagrams.layoutdata.EdgeLayoutData;
4445
import org.eclipse.sirius.components.diagrams.layoutdata.LabelLayoutData;
@@ -171,7 +172,14 @@ private Diagram doRender(Object targetObject, IEditingContext editingContext, Di
171172

172173
Diagram newDiagram = new DiagramRenderer().render(element);
173174

175+
List<DiagramNodeLayoutEvent> diagramNodeLayoutEvents = diagramEvents.stream()
176+
.filter(DiagramNodeLayoutEvent.class::isInstance)
177+
.map(DiagramNodeLayoutEvent.class::cast).toList();
178+
174179
var newLayoutData = optionalPreviousDiagram.map(Diagram::getLayoutData).orElse(new DiagramLayoutData(Map.of(), Map.of(), Map.of()));
180+
181+
diagramNodeLayoutEvents.forEach(nodeLayoutDataEvent -> newLayoutData.nodeLayoutData().put(nodeLayoutDataEvent.nodeId(), nodeLayoutDataEvent.nodeLayoutData()));
182+
175183
newDiagram = Diagram.newDiagram(newDiagram)
176184
.layoutData(newLayoutData)
177185
.build();

packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/DiagramEventProcessor.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2019, 2025 Obeo.
2+
* Copyright (c) 2019, 2026 Obeo.
33
* This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v2.0
55
* which accompanies this distribution, and is available at
@@ -128,16 +128,16 @@ public ISubscriptionManager getSubscriptionManager() {
128128
public void handle(One<IPayload> payloadSink, Many<ChangeDescription> changeDescriptionSink, IRepresentationInput representationInput) {
129129
if (representationInput instanceof LayoutDiagramInput layoutDiagramInput) {
130130
if (LayoutDiagramInput.CAUSE_LAYOUT.equals(layoutDiagramInput.cause()) || layoutDiagramInput.id().equals(this.currentRevisionId)) {
131+
var changeDescription = new ChangeDescription(DiagramChangeKind.DIAGRAM_LAYOUT_CHANGE, editingContext.getId(), representationInput);
132+
this.diagramEventConsumers.forEach(consumer -> consumer.accept(this.editingContext, this.diagramContext.diagram(), this.diagramContext.diagramEvents(), this.diagramContext.viewDeletionRequests(), this.diagramContext.viewCreationRequests(), changeDescription));
131133
var diagram = this.diagramContext.diagram();
132134
var laidOutDiagram = this.diagramCreationService.updateLayout(this.editingContext, diagram, layoutDiagramInput);
133135

134136
this.representationPersistenceService.save(layoutDiagramInput, this.editingContext, laidOutDiagram);
135137
this.diagramContext = new DiagramContext(laidOutDiagram);
136138
this.diagramEventFlux.diagramRefreshed(layoutDiagramInput.id(), laidOutDiagram, DiagramRefreshedEventPayload.CAUSE_LAYOUT, null);
137-
138139
this.currentRevisionCause = DiagramRefreshedEventPayload.CAUSE_LAYOUT;
139140
this.currentRevisionId = layoutDiagramInput.id();
140-
141141
payloadSink.tryEmitValue(new SuccessPayload(layoutDiagramInput.id()));
142142
} else {
143143
payloadSink.tryEmitValue(new SuccessPayload(layoutDiagramInput.id()));
@@ -162,7 +162,6 @@ public void handle(One<IPayload> payloadSink, Many<ChangeDescription> changeDesc
162162
public void refresh(ChangeDescription changeDescription) {
163163
if (this.shouldRefresh(changeDescription)) {
164164
this.diagramEventConsumers.forEach(consumer -> consumer.accept(this.editingContext, this.diagramContext.diagram(), this.diagramContext.diagramEvents(), this.diagramContext.viewDeletionRequests(), this.diagramContext.viewCreationRequests(), changeDescription));
165-
166165
Diagram refreshedDiagram = this.diagramCreationService.refresh(this.editingContext, this.diagramContext).orElse(null);
167166
this.representationPersistenceService.save(changeDescription.getInput(), this.editingContext, refreshedDiagram);
168167

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025, 2026 Obeo.
3+
* This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Obeo - initial API and implementation
12+
*******************************************************************************/
13+
package org.eclipse.sirius.components.diagrams.events.undoredo;
14+
15+
import org.eclipse.sirius.components.diagrams.events.IDiagramEvent;
16+
import org.eclipse.sirius.components.diagrams.layoutdata.NodeLayoutData;
17+
18+
/**
19+
* Diagram node layout position event.
20+
*
21+
* @author mcharfadi
22+
*/
23+
public record DiagramNodeLayoutEvent(String nodeId, NodeLayoutData nodeLayoutData) implements IDiagramEvent {
24+
}

packages/diagrams/frontend/sirius-components-diagrams/src/renderer/DiagramRenderer.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2023, 2025 Obeo.
2+
* Copyright (c) 2023, 2026 Obeo.
33
* This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v2.0
55
* which accompanies this distribution, and is available at
@@ -157,7 +157,6 @@ export const DiagramRenderer = memo(({ diagramRefreshedEventPayload }: DiagramRe
157157
return convertedNode;
158158
}
159159
});
160-
161160
const { nodeLookup, edgeLookup } = store.getState();
162161
if (cause === 'layout') {
163162
// Apply the new graphical selection, either from the applicable selectionFromTool, or from the previous state of the diagram
@@ -214,6 +213,21 @@ export const DiagramRenderer = memo(({ diagramRefreshedEventPayload }: DiagramRe
214213
nodes,
215214
edges,
216215
};
216+
217+
// If we're refreshing the diagram because of an undo/redo operation we need to update the previous diagram with nodeLayoutData before performing the layout
218+
previousDiagram.nodes = previousDiagram.nodes.map((previousNode) => {
219+
const nodeLayoutData = diagramRefreshedEventPayload.diagram.layoutData.nodeLayoutData.find(
220+
(layoutData) => layoutData.id === previousNode.id
221+
);
222+
if (nodeLayoutData) {
223+
previousNode.position.x = nodeLayoutData.position.x;
224+
previousNode.position.y = nodeLayoutData.position.y;
225+
previousNode.width = nodeLayoutData.size.width;
226+
previousNode.height = nodeLayoutData.size.height;
227+
}
228+
return previousNode;
229+
});
230+
217231
layout(
218232
previousDiagram,
219233
convertedDiagram,

packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout-events/useLayoutOnBoundsChange.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2023, 2025 Obeo.
2+
* Copyright (c) 2023, 2026 Obeo.
33
* This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v2.0
55
* which accompanies this distribution, and is available at
@@ -116,12 +116,26 @@ export const useLayoutOnBoundsChange = (): UseLayoutOnBoundsChangeValue => {
116116
edges: laidOutDiagram.edges,
117117
};
118118

119-
synchronizeLayoutData(crypto.randomUUID(), 'layout', finalDiagram);
119+
var id = crypto.randomUUID();
120+
synchronizeLayoutData(id, 'layout', finalDiagram);
121+
addUndoForLayout(id);
120122
});
121123
}
122124
},
123125
[synchronizeLayoutData, getNodes]
124126
);
125127

128+
const addUndoForLayout = (mutationId: string) => {
129+
var storedUndoStack = sessionStorage.getItem('undoStack');
130+
var storedRedoStack = sessionStorage.getItem('redoStack');
131+
132+
if (storedUndoStack && storedRedoStack) {
133+
var undoStack: String[] = JSON.parse(storedUndoStack);
134+
if (!undoStack.find((id) => id === mutationId)) {
135+
sessionStorage.setItem('undoStack', JSON.stringify([mutationId, ...undoStack]));
136+
}
137+
}
138+
};
139+
126140
return { layoutOnBoundsChange };
127141
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025, 2026 Obeo.
3+
* This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Obeo - initial API and implementation
12+
*******************************************************************************/
13+
package org.eclipse.sirius.web.application.undo.services.changes;
14+
15+
import java.util.List;
16+
import java.util.UUID;
17+
18+
import org.eclipse.sirius.components.diagrams.events.undoredo.DiagramNodeLayoutEvent;
19+
20+
/**
21+
* Used to record changes for node layout.
22+
*
23+
* @author mcharfadi
24+
*/
25+
public record DiagramNodeLayoutChange(UUID inputId, String representationId, List<DiagramNodeLayoutEvent> undoEvents, List<DiagramNodeLayoutEvent> redoChanges) implements IDiagramChange {
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025, 2026 Obeo.
3+
* This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Obeo - initial API and implementation
12+
*******************************************************************************/
13+
package org.eclipse.sirius.web.application.undo.services.handler;
14+
15+
import java.util.Objects;
16+
import java.util.UUID;
17+
18+
import org.eclipse.sirius.components.collaborative.diagrams.DiagramEventProcessor;
19+
import org.eclipse.sirius.components.collaborative.representations.api.IRepresentationEventProcessorRegistry;
20+
import org.eclipse.sirius.components.core.api.IEditingContext;
21+
import org.eclipse.sirius.web.application.editingcontext.EditingContext;
22+
import org.eclipse.sirius.web.application.undo.services.api.IRepresentationChangeHandler;
23+
import org.eclipse.sirius.web.application.undo.services.changes.DiagramNodeLayoutChange;
24+
import org.springframework.stereotype.Service;
25+
26+
/**
27+
* Use to handle the undo/redo for the FadeDiagramElementEvent.
28+
*
29+
* @author mcharfadi
30+
*/
31+
@Service
32+
public class DiagramNodeLayoutChangeHandler implements IRepresentationChangeHandler {
33+
34+
private final IRepresentationEventProcessorRegistry representationEventProcessorRegistry;
35+
36+
public DiagramNodeLayoutChangeHandler(IRepresentationEventProcessorRegistry representationEventProcessorRegistry) {
37+
this.representationEventProcessorRegistry = Objects.requireNonNull(representationEventProcessorRegistry);
38+
}
39+
40+
@Override
41+
public boolean canHandle(UUID inputId, IEditingContext editingContext) {
42+
return editingContext instanceof EditingContext siriusEditingContext && siriusEditingContext.getInputId2RepresentationChanges().get(inputId) != null &&
43+
siriusEditingContext.getInputId2RepresentationChanges().get(inputId).stream()
44+
.anyMatch(DiagramNodeLayoutChange.class::isInstance);
45+
}
46+
47+
@Override
48+
public void redo(UUID inputId, IEditingContext editingContext) {
49+
if (editingContext instanceof EditingContext siriusEditingContext) {
50+
siriusEditingContext.getInputId2RepresentationChanges().get(inputId).stream().filter(DiagramNodeLayoutChange.class::isInstance)
51+
.map(DiagramNodeLayoutChange.class::cast)
52+
.forEach(change -> {
53+
var representationEventProcessorEntry = this.representationEventProcessorRegistry.get(editingContext.getId(), change.representationId());
54+
if (representationEventProcessorEntry != null && representationEventProcessorEntry.getRepresentationEventProcessor() instanceof DiagramEventProcessor eventProcessor) {
55+
var diagramContext = eventProcessor.getDiagramContext();
56+
diagramContext.diagramEvents().addAll(change.redoChanges());
57+
}
58+
});
59+
}
60+
}
61+
62+
@Override
63+
public void undo(UUID inputId, IEditingContext editingContext) {
64+
if (editingContext instanceof EditingContext siriusEditingContext) {
65+
siriusEditingContext.getInputId2RepresentationChanges().get(inputId).stream().filter(DiagramNodeLayoutChange.class::isInstance)
66+
.map(DiagramNodeLayoutChange.class::cast)
67+
.forEach(change -> {
68+
var representationEventProcessorEntry = this.representationEventProcessorRegistry.get(editingContext.getId(), change.representationId());
69+
if (representationEventProcessorEntry != null && representationEventProcessorEntry.getRepresentationEventProcessor() instanceof DiagramEventProcessor eventProcessor) {
70+
var diagramContext = eventProcessor.getDiagramContext();
71+
diagramContext.diagramEvents().addAll(change.undoEvents());
72+
}
73+
});
74+
}
75+
}
76+
}

packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/services/handler/RedoEventHandler.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2024, 2025 Obeo.
2+
* Copyright (c) 2024, 2026 Obeo.
33
* This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v2.0
55
* which accompanies this distribution, and is available at
@@ -63,13 +63,12 @@ public void handle(One<IPayload> payloadSink, Many<ChangeDescription> changeDesc
6363
var emfChangeDescription = siriusEditingContext.getInputId2change().get(redoInput.inputId());
6464
if (emfChangeDescription != null) {
6565
emfChangeDescription.applyAndReverse();
66-
changeDescription = new ChangeDescription(ChangeKind.SEMANTIC_CHANGE, editingContext.getId(), input);
67-
payload = new SuccessPayload(input.id());
6866
}
69-
7067
representationEventProcessorChangeHandlers.stream()
7168
.filter(changeHandler -> changeHandler.canHandle(redoInput.inputId(), siriusEditingContext))
7269
.forEach(changeHandler -> changeHandler.redo(redoInput.inputId(), siriusEditingContext));
70+
changeDescription = new ChangeDescription(ChangeKind.SEMANTIC_CHANGE, editingContext.getId(), input);
71+
payload = new SuccessPayload(input.id());
7372
}
7473
payloadSink.tryEmitValue(payload);
7574
changeDescriptionSink.tryEmitNext(changeDescription);

packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/services/handler/UndoEventHandler.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2024, 2025 Obeo.
2+
* Copyright (c) 2024, 2026 Obeo.
33
* This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v2.0
55
* which accompanies this distribution, and is available at
@@ -12,6 +12,9 @@
1212
*******************************************************************************/
1313
package org.eclipse.sirius.web.application.undo.services.handler;
1414

15+
import java.util.List;
16+
import java.util.Objects;
17+
1518
import org.eclipse.sirius.components.collaborative.api.ChangeDescription;
1619
import org.eclipse.sirius.components.collaborative.api.ChangeKind;
1720
import org.eclipse.sirius.components.collaborative.api.IEditingContextEventHandler;
@@ -29,9 +32,6 @@
2932
import reactor.core.publisher.Sinks.Many;
3033
import reactor.core.publisher.Sinks.One;
3134

32-
import java.util.List;
33-
import java.util.Objects;
34-
3535
/**
3636
* Handler used to undo mutations.
3737
*
@@ -64,12 +64,12 @@ public void handle(One<IPayload> payloadSink, Many<ChangeDescription> changeDesc
6464
var emfChangeDescription = siriusEditingContext.getInputId2change().get(undoInput.inputId());
6565
if (emfChangeDescription != null) {
6666
emfChangeDescription.applyAndReverse();
67-
changeDescription = new ChangeDescription(ChangeKind.SEMANTIC_CHANGE, editingContext.getId(), input);
68-
payload = new SuccessPayload(input.id());
6967
}
7068
representationEventProcessorChangeHandlers.stream()
7169
.filter(changeHandler -> changeHandler.canHandle(undoInput.inputId(), siriusEditingContext))
7270
.forEach(changeHandler -> changeHandler.undo(undoInput.inputId(), siriusEditingContext));
71+
changeDescription = new ChangeDescription(ChangeKind.SEMANTIC_CHANGE, editingContext.getId(), input);
72+
payload = new SuccessPayload(input.id());
7373
}
7474
payloadSink.tryEmitValue(payload);
7575
changeDescriptionSink.tryEmitNext(changeDescription);

0 commit comments

Comments
 (0)