Skip to content

Commit 0735ece

Browse files
committed
[5300] Add support for undo redo node layout data
Bug: #5300 Signed-off-by: Michaël Charfadi <michael.charfadi@obeosoft.com>
1 parent f87ba5f commit 0735ece

File tree

14 files changed

+548
-11
lines changed

14 files changed

+548
-11
lines changed

CHANGELOG.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
=== New Features
4242

4343
- https://github.com/eclipse-sirius/sirius-web/issues/5667[#5667] [sirius-web] Add a new left-side panel to perform basic search of semantic elements
44+
- https://github.com/eclipse-sirius/sirius-web/issues/5300[#5300] [diagram] Add undo redo for node layout
45+
4446

4547
=== Improvements
4648

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
@@ -35,6 +35,7 @@
3535
import org.eclipse.sirius.components.diagrams.components.DiagramComponentProps.Builder;
3636
import org.eclipse.sirius.components.diagrams.description.DiagramDescription;
3737
import org.eclipse.sirius.components.diagrams.events.IDiagramEvent;
38+
import org.eclipse.sirius.components.diagrams.events.undoredo.DiagramNodeLayoutEvent;
3839
import org.eclipse.sirius.components.diagrams.layoutdata.DiagramLayoutData;
3940
import org.eclipse.sirius.components.diagrams.renderer.DiagramRenderer;
4041
import org.eclipse.sirius.components.diagrams.renderer.IEdgeAppearanceHandler;
@@ -149,7 +150,14 @@ private Diagram doRender(Object targetObject, IEditingContext editingContext, Di
149150

150151
Diagram newDiagram = new DiagramRenderer().render(element);
151152

153+
List<DiagramNodeLayoutEvent> diagramNodeLayoutEvents = diagramEvents.stream()
154+
.filter(DiagramNodeLayoutEvent.class::isInstance)
155+
.map(DiagramNodeLayoutEvent.class::cast).toList();
156+
152157
var newLayoutData = optionalPreviousDiagram.map(Diagram::getLayoutData).orElse(new DiagramLayoutData(Map.of(), Map.of(), Map.of()));
158+
159+
diagramNodeLayoutEvents.forEach(nodeLayoutDataEvent -> newLayoutData.nodeLayoutData().put(nodeLayoutDataEvent.nodeId(), nodeLayoutDataEvent.nodeLayoutData()));
160+
153161
newDiagram = Diagram.newDiagram(newDiagram)
154162
.layoutData(newLayoutData)
155163
.build();

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ public ISubscriptionManager getSubscriptionManager() {
135135
public void handle(One<IPayload> payloadSink, Many<ChangeDescription> changeDescriptionSink, IRepresentationInput representationInput) {
136136
if (representationInput instanceof LayoutDiagramInput layoutDiagramInput) {
137137
if (LayoutDiagramInput.CAUSE_LAYOUT.equals(layoutDiagramInput.cause()) || layoutDiagramInput.id().equals(this.currentRevisionId)) {
138+
var changeDescription = new ChangeDescription(DiagramChangeKind.DIAGRAM_LAYOUT_CHANGE, editingContext.getId(), representationInput);
139+
this.diagramEventConsumers.forEach(consumer -> consumer.accept(this.editingContext, this.diagramContext.diagram(), this.diagramContext.diagramEvents(), this.diagramContext.viewDeletionRequests(), this.diagramContext.viewCreationRequests(), changeDescription));
138140
var diagram = this.diagramContext.diagram();
139141
var nodeLayoutData = layoutDiagramInput.diagramLayoutData().nodeLayoutData().stream()
140142
.collect(Collectors.toMap(
@@ -165,10 +167,8 @@ public void handle(One<IPayload> payloadSink, Many<ChangeDescription> changeDesc
165167
this.representationPersistenceService.save(layoutDiagramInput, this.editingContext, laidOutDiagram);
166168
this.diagramContext = new DiagramContext(laidOutDiagram);
167169
this.diagramEventFlux.diagramRefreshed(layoutDiagramInput.id(), laidOutDiagram, DiagramRefreshedEventPayload.CAUSE_LAYOUT, null);
168-
169170
this.currentRevisionCause = DiagramRefreshedEventPayload.CAUSE_LAYOUT;
170171
this.currentRevisionId = layoutDiagramInput.id();
171-
172172
payloadSink.tryEmitValue(new SuccessPayload(layoutDiagramInput.id()));
173173
} else {
174174
payloadSink.tryEmitValue(new SuccessPayload(layoutDiagramInput.id()));
@@ -193,7 +193,6 @@ public void handle(One<IPayload> payloadSink, Many<ChangeDescription> changeDesc
193193
public void refresh(ChangeDescription changeDescription) {
194194
if (this.shouldRefresh(changeDescription)) {
195195
this.diagramEventConsumers.forEach(consumer -> consumer.accept(this.editingContext, this.diagramContext.diagram(), this.diagramContext.diagramEvents(), this.diagramContext.viewDeletionRequests(), this.diagramContext.viewCreationRequests(), changeDescription));
196-
197196
Diagram refreshedDiagram = this.diagramCreationService.refresh(this.editingContext, this.diagramContext).orElse(null);
198197
this.representationPersistenceService.save(changeDescription.getInput(), this.editingContext, refreshedDiagram);
199198

@@ -202,7 +201,6 @@ public void refresh(ChangeDescription changeDescription) {
202201
}
203202

204203
this.diagramContext = new DiagramContext(refreshedDiagram);
205-
206204
this.currentRevisionId = changeDescription.getInput().id();
207205
this.currentRevisionCause = DiagramRefreshedEventPayload.CAUSE_REFRESH;
208206

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 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: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,6 @@ export const DiagramRenderer = memo(({ diagramRefreshedEventPayload }: DiagramRe
150150
return convertedNode;
151151
}
152152
});
153-
154153
const { nodeLookup, edgeLookup } = store.getState();
155154
if (cause === 'layout') {
156155
// Apply the new graphical selection, either from the applicable selectionFromTool, or from the previous state of the diagram
@@ -203,6 +202,21 @@ export const DiagramRenderer = memo(({ diagramRefreshedEventPayload }: DiagramRe
203202
nodes,
204203
edges,
205204
};
205+
206+
// 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
207+
previousDiagram.nodes = previousDiagram.nodes.map((previousNode) => {
208+
const nodeLayoutData = diagramRefreshedEventPayload.diagram.layoutData.nodeLayoutData.find(
209+
(layoutData) => layoutData.id === previousNode.id
210+
);
211+
if (nodeLayoutData) {
212+
previousNode.position.x = nodeLayoutData.position.x;
213+
previousNode.position.y = nodeLayoutData.position.y;
214+
previousNode.width = nodeLayoutData.size.width;
215+
previousNode.height = nodeLayoutData.size.height;
216+
}
217+
return previousNode;
218+
});
219+
206220
layout(previousDiagram, convertedDiagram, diagramRefreshedEventPayload.referencePosition, (laidOutDiagram) => {
207221
laidOutDiagram.nodes = laidOutDiagram.nodes.map((node) => {
208222
if (nodeLookup.get(node.id)) {

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,26 @@ export const useLayoutOnBoundsChange = (): UseLayoutOnBoundsChangeValue => {
131131
edges: laidOutDiagram.edges,
132132
};
133133

134-
synchronizeLayoutData(crypto.randomUUID(), 'layout', finalDiagram);
134+
var id = crypto.randomUUID();
135+
synchronizeLayoutData(id, 'layout', finalDiagram);
136+
addUndoForLayout(id);
135137
});
136138
}
137139
},
138140
[synchronizeLayoutData, getNodes]
139141
);
140142

143+
const addUndoForLayout = (mutationId: string) => {
144+
var storedUndoStack = sessionStorage.getItem('undoStack');
145+
var storedRedoStack = sessionStorage.getItem('redoStack');
146+
147+
if (storedUndoStack && storedRedoStack) {
148+
var undoStack: String[] = JSON.parse(storedUndoStack);
149+
if (!undoStack.find((id) => id === mutationId)) {
150+
sessionStorage.setItem('undoStack', JSON.stringify([mutationId, ...undoStack]));
151+
}
152+
}
153+
};
154+
141155
return { layoutOnBoundsChange };
142156
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 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 org.eclipse.sirius.components.diagrams.events.undoredo.DiagramNodeLayoutEvent;
16+
17+
import java.util.List;
18+
import java.util.UUID;
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> undoChanges, List<DiagramNodeLayoutEvent> redoChanges) implements IDiagramChange {
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 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 org.eclipse.sirius.components.collaborative.diagrams.DiagramEventProcessor;
16+
import org.eclipse.sirius.components.collaborative.representations.api.IRepresentationEventProcessorRegistry;
17+
import org.eclipse.sirius.components.core.api.IEditingContext;
18+
import org.eclipse.sirius.components.diagrams.events.undoredo.DiagramNodeLayoutEvent;
19+
import org.eclipse.sirius.web.application.editingcontext.EditingContext;
20+
import org.eclipse.sirius.web.application.undo.services.api.IRepresentationChangeHandler;
21+
import org.eclipse.sirius.web.application.undo.services.changes.DiagramNodeLayoutChange;
22+
import org.springframework.stereotype.Service;
23+
24+
import java.util.Objects;
25+
import java.util.UUID;
26+
27+
/**
28+
* Use to handle the undo/redo for the FadeDiagramElementEvent.
29+
*
30+
* @author mcharfadi
31+
*/
32+
@Service
33+
public class DiagramNodeLayoutChangeHandler implements IRepresentationChangeHandler {
34+
35+
private final IRepresentationEventProcessorRegistry representationEventProcessorRegistry;
36+
37+
public DiagramNodeLayoutChangeHandler(IRepresentationEventProcessorRegistry representationEventProcessorRegistry) {
38+
this.representationEventProcessorRegistry = Objects.requireNonNull(representationEventProcessorRegistry);
39+
}
40+
41+
@Override
42+
public boolean canHandle(UUID inputId, IEditingContext editingContext) {
43+
return editingContext instanceof EditingContext siriusEditingContext && siriusEditingContext.getInputId2RepresentationChanges().get(inputId) != null &&
44+
siriusEditingContext.getInputId2RepresentationChanges().get(inputId).stream()
45+
.anyMatch(DiagramNodeLayoutChange.class::isInstance);
46+
}
47+
48+
@Override
49+
public void redo(UUID inputId, IEditingContext editingContext) {
50+
if (editingContext instanceof EditingContext siriusEditingContext) {
51+
siriusEditingContext.getInputId2RepresentationChanges().get(inputId).stream().filter(DiagramNodeLayoutChange.class::isInstance)
52+
.map(DiagramNodeLayoutChange.class::cast)
53+
.forEach(change -> {
54+
var representationEventProcessorEntry = this.representationEventProcessorRegistry.get(editingContext.getId(), change.representationId());
55+
if (representationEventProcessorEntry != null && representationEventProcessorEntry.getRepresentationEventProcessor() instanceof DiagramEventProcessor eventProcessor) {
56+
var diagramContext = eventProcessor.getDiagramContext();
57+
change.redoChanges().forEach(nodeLayoutChange -> {
58+
diagramContext.diagramEvents().add(new DiagramNodeLayoutEvent(nodeLayoutChange.nodeId(), nodeLayoutChange.nodeLayoutData()));
59+
});
60+
}
61+
});
62+
}
63+
}
64+
65+
@Override
66+
public void undo(UUID inputId, IEditingContext editingContext) {
67+
if (editingContext instanceof EditingContext siriusEditingContext) {
68+
siriusEditingContext.getInputId2RepresentationChanges().get(inputId).stream().filter(DiagramNodeLayoutChange.class::isInstance)
69+
.map(DiagramNodeLayoutChange.class::cast)
70+
.forEach(change -> {
71+
var representationEventProcessorEntry = this.representationEventProcessorRegistry.get(editingContext.getId(), change.representationId());
72+
if (representationEventProcessorEntry != null && representationEventProcessorEntry.getRepresentationEventProcessor() instanceof DiagramEventProcessor eventProcessor) {
73+
var diagramContext = eventProcessor.getDiagramContext();
74+
change.undoChanges().forEach(nodeLayoutChange -> {
75+
diagramContext.diagramEvents().add(new DiagramNodeLayoutEvent(nodeLayoutChange.nodeId(), nodeLayoutChange.nodeLayoutData()));
76+
});
77+
}
78+
});
79+
}
80+
}
81+
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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)