diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index b8985b3af6..c7fcb49a57 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -77,6 +77,7 @@ - https://github.com/eclipse-sirius/sirius-components/issues/1026[#1026] [compatibility] Add support for `OperationAction`. The action are converted to regular tools available in the palette of the frontend - https://github.com/eclipse-sirius/sirius-components/issues/937[#937] [diagram] Add the ability to export diagram as SVG images - https://github.com/eclipse-sirius/sirius-components/issues/779[#779] [diagram] Add support for tools preconditions +- https://github.com/eclipse-sirius/sirius-components/issues/781[#781] [diagram] Add support for multiline labels == v2022.01.0 diff --git a/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/export/svg/DiagramElementExportService.java b/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/export/svg/DiagramElementExportService.java index 56532edaa3..43b9433f15 100644 --- a/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/export/svg/DiagramElementExportService.java +++ b/backend/sirius-components-collaborative-diagrams/src/main/java/org/eclipse/sirius/components/collaborative/diagrams/export/svg/DiagramElementExportService.java @@ -54,7 +54,7 @@ public StringBuilder exportLabel(Label label) { labelExport.append(this.exportImageElement(style.getIconURL(), -20, -12, Optional.empty())); } - labelExport.append(this.exportTextElement(label.getText(), style)); + labelExport.append(this.exportTextElement(label.getText(), label.getType(), style)); return labelExport.append(""); //$NON-NLS-1$ } @@ -109,16 +109,32 @@ private StringBuilder addSizeParam(Size size) { return sizeParam.append("height=\"" + size.getHeight() + "\" "); //$NON-NLS-1$ //$NON-NLS-2$ } - private StringBuilder exportTextElement(String text, LabelStyle labelStyle) { + private StringBuilder exportTextElement(String text, String type, LabelStyle labelStyle) { StringBuilder textExport = new StringBuilder(); textExport.append(""); //$NON-NLS-1$ - textExport.append(text); + String[] lines = text.split("\\n", -1); //$NON-NLS-1$ + if (lines.length == 1) { + textExport.append(text); + } else { + textExport.append("" + lines[0] + ""); //$NON-NLS-1$//$NON-NLS-2$ + double fontSize = labelStyle.getFontSize(); + for (int i = 1; i < lines.length; i++) { + if (lines[i].isEmpty()) { + // avoid tspan to be ignored if there is only a line return + lines[i] = " "; //$NON-NLS-1$ + } + textExport.append("" + lines[i] + ""); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ + } + } return textExport.append(""); //$NON-NLS-1$ } diff --git a/backend/sirius-components-collaborative-forms/src/main/java/org/eclipse/sirius/components/collaborative/forms/handlers/EditTextfieldEventHandler.java b/backend/sirius-components-collaborative-forms/src/main/java/org/eclipse/sirius/components/collaborative/forms/handlers/EditTextfieldEventHandler.java index 64461cef72..3985ceea50 100644 --- a/backend/sirius-components-collaborative-forms/src/main/java/org/eclipse/sirius/components/collaborative/forms/handlers/EditTextfieldEventHandler.java +++ b/backend/sirius-components-collaborative-forms/src/main/java/org/eclipse/sirius/components/collaborative/forms/handlers/EditTextfieldEventHandler.java @@ -13,6 +13,7 @@ package org.eclipse.sirius.components.collaborative.forms.handlers; import java.util.Objects; +import java.util.function.Function; import org.eclipse.sirius.components.collaborative.api.ChangeDescription; import org.eclipse.sirius.components.collaborative.api.ChangeKind; @@ -26,6 +27,7 @@ import org.eclipse.sirius.components.core.api.ErrorPayload; import org.eclipse.sirius.components.core.api.IPayload; import org.eclipse.sirius.components.forms.Form; +import org.eclipse.sirius.components.forms.Textarea; import org.eclipse.sirius.components.forms.Textfield; import org.eclipse.sirius.components.representations.Failure; import org.eclipse.sirius.components.representations.IStatus; @@ -78,11 +80,16 @@ public void handle(One payloadSink, Many changeDesc EditTextfieldInput input = (EditTextfieldInput) formInput; // @formatter:off - var optionalTextfield = this.formQueryService.findWidget(form, input.getTextfieldId()) - .filter(Textfield.class::isInstance) - .map(Textfield.class::cast); - - IStatus status = optionalTextfield.map(Textfield::getNewValueHandler) + IStatus status = this.formQueryService.findWidget(form, input.getTextfieldId()) + .map(widget -> { + Function handlerFunction = null; + if (widget instanceof Textfield) { + handlerFunction = ((Textfield) widget).getNewValueHandler(); + } else if (widget instanceof Textarea) { + handlerFunction = ((Textarea) widget).getNewValueHandler(); + } + return handlerFunction; + }) .map(handler -> handler.apply(input.getNewValue())) .orElse(new Failure("")); //$NON-NLS-1$ // @formatter:on diff --git a/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/ELKDiagramConverter.java b/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/ELKDiagramConverter.java index b2da021861..ba6d6f8364 100644 --- a/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/ELKDiagramConverter.java +++ b/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/ELKDiagramConverter.java @@ -320,8 +320,8 @@ private void convertNode(Node node, ElkNode parent, Map childNodes = this.getLayoutedNodes(node.getChildNodes(), id2ElkGraphElements); List borderNodes = this.getLayoutedNodes(node.getBorderNodes(), id2ElkGraphElements); + // @formatter:off return Node.newNode(node) .label(label) .size(size) diff --git a/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/incremental/IncrementalLayoutEngine.java b/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/incremental/IncrementalLayoutEngine.java index 118b16842b..cb648135fa 100644 --- a/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/incremental/IncrementalLayoutEngine.java +++ b/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/incremental/IncrementalLayoutEngine.java @@ -152,11 +152,11 @@ private void layoutNode(Optional optionalDiagramElementEvent, Nod // update the border node once the current node bounds are updated Bounds newBounds = Bounds.newBounds().position(node.getPosition()).size(node.getSize()).build(); - this.layoutBorderNodes(optionalDiagramElementEvent, node.getBorderNodes(), initialNodeBounds, newBounds, layoutConfigurator); + List borderNodesOnSide = this.layoutBorderNodes(optionalDiagramElementEvent, node.getBorderNodes(), initialNodeBounds, newBounds, layoutConfigurator); // recompute the label if (node.getLabel() != null) { - node.getLabel().setPosition(this.nodeLabelPositionProvider.getPosition(node, node.getLabel())); + node.getLabel().setPosition(this.nodeLabelPositionProvider.getPosition(node, node.getLabel(), borderNodesOnSide)); } } @@ -164,8 +164,9 @@ private void layoutNode(Optional optionalDiagramElementEvent, Nod * Update the border nodes position according to the side length change where it is located.
* The aim is to keep the positioning ratio of the border node on its side. */ - private void layoutBorderNodes(Optional optionalDiagramElementEvent, List borderNodesLayoutData, Bounds initialNodeBounds, Bounds newNodeBounds, + private List layoutBorderNodes(Optional optionalDiagramElementEvent, List borderNodesLayoutData, Bounds initialNodeBounds, Bounds newNodeBounds, ISiriusWebLayoutConfigurator layoutConfigurator) { + List borderNodesPerSide = new ArrayList<>(); if (!borderNodesLayoutData.isEmpty()) { for (NodeLayoutData nodeLayoutData : borderNodesLayoutData) { // 1- update the position of the border node if it has been explicitly moved @@ -179,7 +180,7 @@ private void layoutBorderNodes(Optional optionalDiagramElementEve } // 2- recompute the border node - List borderNodesPerSide = this.snapBorderNodes(borderNodesLayoutData, initialNodeBounds.getSize(), layoutConfigurator); + borderNodesPerSide = this.snapBorderNodes(borderNodesLayoutData, initialNodeBounds.getSize(), layoutConfigurator); // 3 - move the border node along the side according to the side change this.updateBorderNodeAccordingParentResize(optionalDiagramElementEvent, initialNodeBounds, newNodeBounds, borderNodesPerSide, borderNodesLayoutData.get(0).getParent().getId()); @@ -187,6 +188,7 @@ private void layoutBorderNodes(Optional optionalDiagramElementEve // 4- set the label position if the border is newly created this.updateBorderNodeLabel(optionalDiagramElementEvent, borderNodesPerSide); } + return borderNodesPerSide; } private void updateBorderNodeLabel(Optional optionalDiagramElementEvent, List borderNodesPerSideList) { diff --git a/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/incremental/provider/NodeLabelPositionProvider.java b/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/incremental/provider/NodeLabelPositionProvider.java index 59d71555e9..719f7acd6b 100644 --- a/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/incremental/provider/NodeLabelPositionProvider.java +++ b/backend/sirius-components-diagrams-layout/src/main/java/org/eclipse/sirius/components/diagrams/layout/incremental/provider/NodeLabelPositionProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021 THALES GLOBAL SERVICES. + * Copyright (c) 2021, 2022 THALES GLOBAL SERVICES. * This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -13,6 +13,7 @@ package org.eclipse.sirius.components.diagrams.layout.incremental.provider; import java.util.EnumSet; +import java.util.List; import java.util.Objects; import org.eclipse.elk.core.math.ElkPadding; @@ -21,8 +22,10 @@ import org.eclipse.sirius.components.diagrams.NodeType; import org.eclipse.sirius.components.diagrams.Position; import org.eclipse.sirius.components.diagrams.layout.ISiriusWebLayoutConfigurator; +import org.eclipse.sirius.components.diagrams.layout.incremental.BorderNodesOnSide; import org.eclipse.sirius.components.diagrams.layout.incremental.data.LabelLayoutData; import org.eclipse.sirius.components.diagrams.layout.incremental.data.NodeLayoutData; +import org.eclipse.sirius.components.diagrams.layout.incremental.utils.RectangleSide; /** * Provides the position to apply to a Node Label. @@ -37,7 +40,7 @@ public NodeLabelPositionProvider(ISiriusWebLayoutConfigurator layoutConfigurator this.layoutConfigurator = Objects.requireNonNull(layoutConfigurator); } - public Position getPosition(NodeLayoutData node, LabelLayoutData label) { + public Position getPosition(NodeLayoutData node, LabelLayoutData label, List borderNodesOnSide) { double x = 0d; double y = 0d; @@ -49,7 +52,7 @@ public Position getPosition(NodeLayoutData node, LabelLayoutData label) { } break; default: - x = this.getHorizontalPosition(node, label); + x = this.getHorizontalPosition(node, label, borderNodesOnSide); y = this.getVerticalPosition(node, label); break; } @@ -57,7 +60,7 @@ public Position getPosition(NodeLayoutData node, LabelLayoutData label) { return Position.at(x, y); } - private double getHorizontalPosition(NodeLayoutData node, LabelLayoutData label) { + private double getHorizontalPosition(NodeLayoutData node, LabelLayoutData label, List borderNodesOnSides) { double x = 0d; EnumSet nodeLabelPlacementSet = this.layoutConfigurator.configureByType(node.getNodeType()).getProperty(CoreOptions.NODE_LABELS_PLACEMENT); ElkPadding nodeLabelsPadding = this.layoutConfigurator.configureByType(node.getNodeType()).getProperty(CoreOptions.NODE_LABELS_PADDING); @@ -79,7 +82,21 @@ private double getHorizontalPosition(NodeLayoutData node, LabelLayoutData label) } break; case H_CENTER: - x = (node.getSize().getWidth() - label.getTextBounds().getSize().getWidth()) / 2; + // The label is positioned at the center of the node and the front-end will apply a "'text-anchor': + // 'middle'" property. + int shiftToEast = 0; + int shiftToWest = 0; + for (BorderNodesOnSide borderNodesOnSide : borderNodesOnSides) { + if (RectangleSide.WEST.equals(borderNodesOnSide.getSide())) { + shiftToEast = 1; + } else if (RectangleSide.EAST.equals(borderNodesOnSide.getSide())) { + shiftToWest = 1; + } + } + double portOffset = this.layoutConfigurator.configureByType(node.getNodeType()).getProperty(CoreOptions.PORT_BORDER_OFFSET).doubleValue(); + double offSetAccordingToBorderNodes = -portOffset / 2 * (shiftToEast - shiftToWest); + + x = node.getSize().getWidth() / 2 + offSetAccordingToBorderNodes; break; case H_RIGHT: if (outside) { diff --git a/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/incremental/NodeLabelPositionProviderTests.java b/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/incremental/NodeLabelPositionProviderTests.java index 48145afe18..29528ed6ef 100644 --- a/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/incremental/NodeLabelPositionProviderTests.java +++ b/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/incremental/NodeLabelPositionProviderTests.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021 THALES GLOBAL SERVICES. + * Copyright (c) 2021, 2022 THALES GLOBAL SERVICES. * This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -14,6 +14,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -61,8 +62,8 @@ public void testNodeImageLabelBoundsPosition() { NodeLabelPositionProvider labelBoundsProvider = new NodeLabelPositionProvider(new LayoutConfiguratorRegistry(List.of()).getDefaultLayoutConfigurator()); LabelLayoutData labelLayoutData = this.createLabelLayoutData(); - Position position = labelBoundsProvider.getPosition(nodeLayoutData, labelLayoutData); - assertThat(position).extracting(Position::getX).isEqualTo(Double.valueOf(42.5390625)); + Position position = labelBoundsProvider.getPosition(nodeLayoutData, labelLayoutData, new ArrayList<>()); + assertThat(position).extracting(Position::getX).isEqualTo(Double.valueOf(DEFAULT_NODE_SIZE.getWidth() / 2)); assertThat(position).extracting(Position::getY).isEqualTo(Double.valueOf(-23.3984375)); } @@ -72,8 +73,8 @@ public void testNodeRectangleLabelBoundsPosition() { NodeLayoutData nodeLayoutData = this.createNodeLayoutData(Position.at(0, 0), DEFAULT_NODE_SIZE, createDiagramLayoutData, NodeType.NODE_RECTANGLE); NodeLabelPositionProvider labelBoundsProvider = new NodeLabelPositionProvider(new LayoutConfiguratorRegistry(List.of()).getDefaultLayoutConfigurator()); LabelLayoutData labelLayoutData = this.createLabelLayoutData(); - Position position = labelBoundsProvider.getPosition(nodeLayoutData, labelLayoutData); - assertThat(position).extracting(Position::getX).isEqualTo(Double.valueOf(42.5390625)); + Position position = labelBoundsProvider.getPosition(nodeLayoutData, labelLayoutData, new ArrayList<>()); + assertThat(position).extracting(Position::getX).isEqualTo(Double.valueOf(DEFAULT_NODE_SIZE.getWidth() / 2)); assertThat(position).extracting(Position::getY).isEqualTo(Double.valueOf(5)); } diff --git a/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/services/DiagramELKLayoutTest.java b/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/services/DiagramELKLayoutTest.java new file mode 100644 index 0000000000..3bfed03598 --- /dev/null +++ b/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/services/DiagramELKLayoutTest.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * Copyright (c) 2022 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.components.diagrams.layout.services; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IRepresentationDescriptionSearchService; +import org.eclipse.sirius.components.diagrams.Diagram; +import org.eclipse.sirius.components.diagrams.Node; +import org.eclipse.sirius.components.diagrams.Position; +import org.eclipse.sirius.components.diagrams.Size; +import org.eclipse.sirius.components.diagrams.description.DiagramDescription; +import org.eclipse.sirius.components.diagrams.layout.ELKDiagramConverter; +import org.eclipse.sirius.components.diagrams.layout.ELKLayoutedDiagramProvider; +import org.eclipse.sirius.components.diagrams.layout.LayoutConfiguratorRegistry; +import org.eclipse.sirius.components.diagrams.layout.LayoutService; +import org.eclipse.sirius.components.diagrams.layout.TextBoundsService; +import org.eclipse.sirius.components.diagrams.layout.incremental.IncrementalLayoutDiagramConverter; +import org.eclipse.sirius.components.diagrams.layout.incremental.IncrementalLayoutEngine; +import org.eclipse.sirius.components.diagrams.layout.incremental.IncrementalLayoutedDiagramProvider; +import org.eclipse.sirius.components.diagrams.layout.incremental.provider.ImageSizeProvider; +import org.eclipse.sirius.components.diagrams.layout.incremental.provider.NodeSizeProvider; +import org.eclipse.sirius.components.diagrams.tests.builder.JsonBasedEditingContext; +import org.eclipse.sirius.components.diagrams.tests.builder.TestLayoutDiagramBuilder; +import org.eclipse.sirius.components.representations.IRepresentationDescription; +import org.junit.jupiter.api.Test; + +/** + * Used to test the diagram full layout. + * + * @author lfasani + */ +public class DiagramELKLayoutTest { + + private TestLayoutObjectService objectService = new TestLayoutObjectService(); + + private DefaultTestDiagramDescriptionProvider defaultTestDiagramDescriptionProvider = new DefaultTestDiagramDescriptionProvider(this.objectService); + + private Optional getNode(List nodes, String targetObjectId) { + Optional optionalNode = Optional.empty(); + List deeperNode = new ArrayList<>(); + + Iterator nodeIt = nodes.iterator(); + while (optionalNode.isEmpty() && nodeIt.hasNext()) { + Node node = nodeIt.next(); + if (targetObjectId.equals(node.getTargetObjectId())) { + optionalNode = Optional.of(node); + } else { + deeperNode.addAll(node.getChildNodes()); + } + } + + if (optionalNode.isEmpty() && !deeperNode.isEmpty()) { + optionalNode = this.getNode(deeperNode, targetObjectId); + } + + return optionalNode; + } + + private TestDiagramCreationService createDiagramCreationService(Diagram diagram) { + IRepresentationDescriptionSearchService.NoOp representationDescriptionSearchService = new IRepresentationDescriptionSearchService.NoOp() { + @Override + public Optional findById(IEditingContext editingContext, UUID representationDescriptionId) { + DiagramDescription diagramDescription = DiagramELKLayoutTest.this.defaultTestDiagramDescriptionProvider.getDefaultDiagramDescription(diagram); + return Optional.of(diagramDescription); + } + }; + + NodeSizeProvider nodeSizeProvider = new NodeSizeProvider(new ImageSizeProvider()); + IncrementalLayoutEngine incrementalLayoutEngine = new IncrementalLayoutEngine(nodeSizeProvider); + + LayoutService layoutService = new LayoutService(new ELKDiagramConverter(new TextBoundsService(), new ImageSizeProvider()), new IncrementalLayoutDiagramConverter(), + new LayoutConfiguratorRegistry(List.of()), new ELKLayoutedDiagramProvider(), new IncrementalLayoutedDiagramProvider(), representationDescriptionSearchService, incrementalLayoutEngine); + + return new TestDiagramCreationService(this.objectService, representationDescriptionSearchService, layoutService); + } + + @Test + public void testNodeLayoutWithMultilineLabel() throws IOException { + String nodeLabelWithMultiple = "First LineAAAAAAAA\nSecond LineBBBBBBBBB"; //$NON-NLS-1$ + String firstChildTargetObjectId = "First child"; //$NON-NLS-1$ + + // @formatter:off + Diagram diagram = TestLayoutDiagramBuilder.diagram("Root") //$NON-NLS-1$ + .nodes() + .rectangleNode(nodeLabelWithMultiple).at(10, 10).of(10, 10) + .childNodes() + .rectangleNode(firstChildTargetObjectId).at(10, 10).of(50, 50).and() + .and() + .and() + .and() + .build(); + // @formatter:on + + Path path = Paths.get("src", "test", "resources", "editing-contexts", "testNodeLayoutWithMultilineLabel"); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$//$NON-NLS-4$ //$NON-NLS-5$ + JsonBasedEditingContext editingContext = new JsonBasedEditingContext(path); + + TestDiagramCreationService diagramCreationService = this.createDiagramCreationService(diagram); + + Diagram layoutedDiagram = diagramCreationService.performElKLayout(editingContext, diagram); + + Node firstParent = layoutedDiagram.getNodes().get(0); + + // Check that the parent node and the label have the right size + assertThat(firstParent.getSize()).isEqualTo(Size.of(195.8818359375, 131.197265625)); + assertThat(firstParent.getLabel().getSize()).isEqualTo(Size.of(161.8818359375, 32.197265625)); + + // Check that the inner node is under the multi line label area + assertThat(firstParent.getChildNodes().get(0).getPosition()).isEqualTo(Position.at(12, 49.197265625)); + } +} diff --git a/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/services/TestDiagramCreationService.java b/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/services/TestDiagramCreationService.java index 3d1c6c3ae2..80f447f9bc 100644 --- a/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/services/TestDiagramCreationService.java +++ b/backend/sirius-components-diagrams-layout/src/test/java/org/eclipse/sirius/components/diagrams/layout/services/TestDiagramCreationService.java @@ -94,4 +94,8 @@ public Diagram performLayout(IEditingContext editingContext, Diagram diagram, ID return this.layoutService.incrementalLayout(editingContext, diagram, Optional.of(diagramEvent)); } + public Diagram performElKLayout(IEditingContext editingContext, Diagram diagram) { + return this.layoutService.layout(editingContext, diagram); + } + } diff --git a/backend/sirius-components-diagrams-layout/src/test/resources/editing-contexts/testNodeLayoutWithMultilineLabel b/backend/sirius-components-diagrams-layout/src/test/resources/editing-contexts/testNodeLayoutWithMultilineLabel new file mode 100644 index 0000000000..639795d459 --- /dev/null +++ b/backend/sirius-components-diagrams-layout/src/test/resources/editing-contexts/testNodeLayoutWithMultilineLabel @@ -0,0 +1,3 @@ +{ + "name": "diag:Root" +} \ No newline at end of file diff --git a/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/TextBoundsProvider.java b/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/TextBoundsProvider.java index 66da40e2e0..c9111deb2d 100644 --- a/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/TextBoundsProvider.java +++ b/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/TextBoundsProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021 THALES GLOBAL SERVICES. + * Copyright (c) 2021, 2022 THALES GLOBAL SERVICES. * This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -41,7 +41,8 @@ public class TextBoundsProvider { private static String fontName; /** - * Computes the text bounds for a label with the given text. + * Computes the text bounds for a label with the given text.
+ * The text bounds take into account the line return contained in text. * * @param labelStyle * the label style @@ -50,18 +51,29 @@ public class TextBoundsProvider { * @return the text bounds */ public TextBounds computeBounds(LabelStyle labelStyle, String text) { - int fontStyle = Font.PLAIN; - if (labelStyle.isBold()) { - fontStyle = fontStyle | Font.BOLD; - } - if (labelStyle.isItalic()) { - fontStyle = fontStyle | Font.ITALIC; + Font font = this.getFont(labelStyle); + + String[] lines = text.split("\\n", -1); //$NON-NLS-1$ + Rectangle2D labelBounds = null; + if (lines.length == 0) { + labelBounds = font.getStringBounds("", FONT_RENDER_CONTEXT); //$NON-NLS-1$ + } else { + labelBounds = font.getStringBounds(lines[0], FONT_RENDER_CONTEXT); + if (lines.length > 1) { + for (int i = 1; i < lines.length; i++) { + String line = lines[i]; + + Rectangle2D lineBounds = font.getStringBounds(line, FONT_RENDER_CONTEXT); + // shift the rectangle under the previous line + lineBounds.setFrame(lineBounds.getX(), lineBounds.getY() + labelBounds.getHeight(), lineBounds.getWidth(), lineBounds.getHeight()); + + labelBounds = labelBounds.createUnion(lineBounds); + } + } } - Font font = new Font(this.getFontName(), fontStyle, labelStyle.getFontSize()); - Rectangle2D stringBounds = font.getStringBounds(text, FONT_RENDER_CONTEXT); - double width = stringBounds.getWidth(); - double height = stringBounds.getHeight(); + double height = labelBounds.getHeight(); + double width = labelBounds.getWidth(); double iconWidth = 0; double iconHeight = 0; if (!labelStyle.getIconURL().isEmpty()) { @@ -74,11 +86,22 @@ public TextBounds computeBounds(LabelStyle labelStyle, String text) { Size size = Size.of(width + iconWidth, height + iconHeight); // Sprotty needs the inverse of the x and y for the alignment, so it's "0 - x" and "0 - y" on purpose - Position alignment = Position.at(0 - stringBounds.getX() + iconWidth, 0 - stringBounds.getY()); + Position alignment = Position.at(0 - labelBounds.getX() + iconWidth, 0 - labelBounds.getY()); return new TextBounds(size, alignment); } + private Font getFont(LabelStyle labelStyle) { + int fontStyle = Font.PLAIN; + if (labelStyle.isBold()) { + fontStyle = fontStyle | Font.BOLD; + } + if (labelStyle.isItalic()) { + fontStyle = fontStyle | Font.ITALIC; + } + return new Font(this.getFontName(), fontStyle, labelStyle.getFontSize()); + } + private String getFontName() { if (fontName == null) { if (this.isDefaultFontAvailable()) { diff --git a/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/components/LabelType.java b/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/components/LabelType.java index 45ea94141f..d6cde43d91 100644 --- a/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/components/LabelType.java +++ b/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/components/LabelType.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021 Obeo. + * Copyright (c) 2021, 2022 Obeo. * This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -19,6 +19,7 @@ */ public enum LabelType { + OUTSIDE("label:outside"), //$NON-NLS-1$ INSIDE_CENTER("label:inside-center"), //$NON-NLS-1$ OUTSIDE_CENTER("label:outside-center"), //$NON-NLS-1$ EDGE_BEGIN("label:edge-begin"), //$NON-NLS-1$ diff --git a/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/components/NodeComponent.java b/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/components/NodeComponent.java index fa64a054f8..24747a8bfd 100644 --- a/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/components/NodeComponent.java +++ b/backend/sirius-components-diagrams/src/main/java/org/eclipse/sirius/components/diagrams/components/NodeComponent.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2021 Obeo and others. + * Copyright (c) 2019, 2022 Obeo and others. * This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -136,7 +136,9 @@ private Element doRender(VariableManager nodeVariableManager, String targetObjec nodeVariableManager.put(LabelDescription.OWNER_ID, nodeId); LabelType nodeLabelType = LabelType.INSIDE_CENTER; - if (NodeType.NODE_IMAGE.equals(type)) { + if (containmentKind == NodeContainmentKind.BORDER_NODE) { + nodeLabelType = LabelType.OUTSIDE; + } else if (NodeType.NODE_IMAGE.equals(type)) { nodeLabelType = LabelType.OUTSIDE_CENTER; } diff --git a/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/compatibility/properties/EStringIfDescriptionProvider.java b/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/compatibility/properties/EStringIfDescriptionProvider.java index 5036f29119..619723a3a7 100644 --- a/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/compatibility/properties/EStringIfDescriptionProvider.java +++ b/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/compatibility/properties/EStringIfDescriptionProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2021 Obeo. + * Copyright (c) 2019, 2022 Obeo. * This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -25,7 +25,7 @@ import org.eclipse.sirius.components.compatibility.forms.WidgetIdProvider; import org.eclipse.sirius.components.emf.compatibility.properties.api.IPropertiesValidationProvider; import org.eclipse.sirius.components.forms.description.IfDescription; -import org.eclipse.sirius.components.forms.description.TextfieldDescription; +import org.eclipse.sirius.components.forms.description.TextareaDescription; import org.eclipse.sirius.components.representations.Failure; import org.eclipse.sirius.components.representations.IStatus; import org.eclipse.sirius.components.representations.Success; @@ -39,7 +39,7 @@ public class EStringIfDescriptionProvider { private static final String IF_DESCRIPTION_ID = "EString"; //$NON-NLS-1$ - private static final String TEXTFIELD_DESCRIPTION_ID = "Textfield"; //$NON-NLS-1$ + private static final String TEXTAREA_DESCRIPTION_ID = "Textarea"; //$NON-NLS-1$ private final ComposedAdapterFactory composedAdapterFactory; @@ -54,7 +54,7 @@ public IfDescription getIfDescription() { // @formatter:off return IfDescription.newIfDescription(IF_DESCRIPTION_ID) .predicate(this.getPredicate()) - .widgetDescription(this.getTextfieldDescription()) + .widgetDescription(this.getTextareaDescription()) .build(); // @formatter:on } @@ -69,9 +69,9 @@ private Function getPredicate() { }; } - private TextfieldDescription getTextfieldDescription() { + private TextareaDescription getTextareaDescription() { // @formatter:off - return TextfieldDescription.newTextfieldDescription(TEXTFIELD_DESCRIPTION_ID) + return TextareaDescription.newTextareaDescription(TEXTAREA_DESCRIPTION_ID) .idProvider(new WidgetIdProvider()) .labelProvider(this.getLabelProvider()) .valueProvider(this.getValueProvider()) diff --git a/backend/sirius-components-forms/src/main/java/org/eclipse/sirius/components/forms/Textarea.java b/backend/sirius-components-forms/src/main/java/org/eclipse/sirius/components/forms/Textarea.java index 480efc75bd..3c62479f78 100644 --- a/backend/sirius-components-forms/src/main/java/org/eclipse/sirius/components/forms/Textarea.java +++ b/backend/sirius-components-forms/src/main/java/org/eclipse/sirius/components/forms/Textarea.java @@ -15,9 +15,11 @@ import java.text.MessageFormat; import java.util.List; import java.util.Objects; +import java.util.function.Function; import org.eclipse.sirius.components.annotations.Immutable; import org.eclipse.sirius.components.forms.validation.Diagnostic; +import org.eclipse.sirius.components.representations.IStatus; /** * The text area widget. @@ -30,6 +32,8 @@ public final class Textarea extends AbstractWidget { private String value; + private Function newValueHandler; + private Textarea() { // Prevent instantiation } @@ -42,6 +46,10 @@ public String getValue() { return this.value; } + public Function getNewValueHandler() { + return this.newValueHandler; + } + public static Builder newTextarea(String id) { return new Builder(id); } @@ -59,13 +67,14 @@ public String toString() { */ @SuppressWarnings("checkstyle:HiddenField") public static final class Builder { - private String id; private String label; private String value; + private Function newValueHandler; + private List diagnostics; private Builder(String id) { @@ -82,6 +91,11 @@ public Builder value(String value) { return this; } + public Builder newValueHandler(Function newValueHandler) { + this.newValueHandler = Objects.requireNonNull(newValueHandler); + return this; + } + public Builder diagnostics(List diagnostics) { this.diagnostics = Objects.requireNonNull(diagnostics); return this; @@ -92,6 +106,7 @@ public Textarea build() { textarea.id = Objects.requireNonNull(this.id); textarea.label = Objects.requireNonNull(this.label); textarea.value = Objects.requireNonNull(this.value); + textarea.newValueHandler = Objects.requireNonNull(this.newValueHandler); textarea.diagnostics = Objects.requireNonNull(this.diagnostics); return textarea; } diff --git a/backend/sirius-components-forms/src/main/java/org/eclipse/sirius/components/forms/renderer/FormElementFactory.java b/backend/sirius-components-forms/src/main/java/org/eclipse/sirius/components/forms/renderer/FormElementFactory.java index 712ac8010a..c0c1fcde79 100644 --- a/backend/sirius-components-forms/src/main/java/org/eclipse/sirius/components/forms/renderer/FormElementFactory.java +++ b/backend/sirius-components-forms/src/main/java/org/eclipse/sirius/components/forms/renderer/FormElementFactory.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2021 Obeo. + * Copyright (c) 2019, 2022 Obeo. * This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -193,6 +193,7 @@ private Textarea instantiateTextarea(TextareaElementProps props, List ch return Textarea.newTextarea(props.getId()) .label(props.getLabel()) .value(props.getValue()) + .newValueHandler(props.getNewValueHandler()) .diagnostics(diagnostics) .build(); // @formatter:on diff --git a/frontend/src/diagram/sprotty/DependencyInjection.ts b/frontend/src/diagram/sprotty/DependencyInjection.ts index 62a079a7fb..a9bfb6c169 100644 --- a/frontend/src/diagram/sprotty/DependencyInjection.ts +++ b/frontend/src/diagram/sprotty/DependencyInjection.ts @@ -10,10 +10,11 @@ * Contributors: * Obeo - initial API and implementation *******************************************************************************/ -import { BorderNode, Node } from 'diagram/sprotty/Diagram.types'; +import { BorderNode, Label, Node } from 'diagram/sprotty/Diagram.types'; import { DiagramServer, HIDE_CONTEXTUAL_TOOLBAR_ACTION, SPROTTY_DELETE_ACTION } from 'diagram/sprotty/DiagramServer'; import { SetActiveConnectorToolsAction, SetActiveToolAction } from 'diagram/sprotty/DiagramServer.types'; import { edgeCreationFeedback } from 'diagram/sprotty/edgeCreationFeedback'; +import { EditLabelUIWithInitialContent } from 'diagram/sprotty/EditLabelUIWithInitialContent'; import { GraphFactory } from 'diagram/sprotty/GraphFactory'; import siriusDragAndDropModule from 'diagram/sprotty/siriusDragAndDropModule'; import { DiagramView } from 'diagram/sprotty/views/DiagramView'; @@ -36,7 +37,6 @@ import { edgeLayoutModule, EditLabelAction, EditLabelActionHandler, - EditLabelUI, exportModule, fadeModule, graphModule, @@ -66,29 +66,6 @@ import { } from 'sprotty'; import { Action, Point, RequestPopupModelAction, SetPopupModelAction, UpdateModelAction } from 'sprotty-protocol'; -/** - * Extends Sprotty's SLabel to add support for having the initial text when entering - * in direct edit mode different from the text's label itself, and makes the - * pre-selection of the edited text optional. - */ -export class SEditableLabel extends SLabel { - initialText: string; - preSelect: boolean = true; -} - -class EditLabelUIWithInitialContent extends EditLabelUI { - protected applyTextContents() { - if (this.label instanceof SEditableLabel) { - this.inputElement.value = this.label.initialText || this.label.text; - if (this.label.preSelect) { - this.inputElement.setSelectionRange(0, this.inputElement.value.length); - } - } else { - super.applyTextContents(); - } - } -} - const labelEditUiModule = new ContainerModule((bind, _unbind, isBound) => { const context = { bind, isBound }; configureActionHandler(context, EditLabelAction.KIND, EditLabelActionHandler); @@ -124,15 +101,17 @@ const siriusWebContainerModule = new ContainerModule((bind, unbind, isBound, reb configureModelElement(context, 'port:image', BorderNode, ImageView); configureView({ bind, isBound }, 'edge:straight', EdgeView); // @ts-ignore - configureModelElement(context, 'label:inside-center', SEditableLabel, LabelView); + configureModelElement(context, 'label:inside-center', Label, LabelView); + // @ts-ignore + configureModelElement(context, 'label:outside-center', Label, LabelView); // @ts-ignore - configureModelElement(context, 'label:outside-center', SEditableLabel, LabelView); + configureModelElement(context, 'label:outside', Label, LabelView); // @ts-ignore - configureModelElement(context, 'label:edge-begin', SEditableLabel, LabelView); + configureModelElement(context, 'label:edge-begin', Label, LabelView); // @ts-ignore - configureModelElement(context, 'label:edge-center', SEditableLabel, LabelView); + configureModelElement(context, 'label:edge-center', Label, LabelView); // @ts-ignore - configureModelElement(context, 'label:edge-end', SEditableLabel, LabelView); + configureModelElement(context, 'label:edge-end', Label, LabelView); // @ts-ignore configureView({ bind, isBound }, 'comp:main', SCompartmentView); configureView({ bind, isBound }, 'html', HtmlRootView); diff --git a/frontend/src/diagram/sprotty/Diagram.types.ts b/frontend/src/diagram/sprotty/Diagram.types.ts index ccdfd69064..3762d0c57d 100644 --- a/frontend/src/diagram/sprotty/Diagram.types.ts +++ b/frontend/src/diagram/sprotty/Diagram.types.ts @@ -105,8 +105,16 @@ export enum ArrowStyle { OutputFillClosedArrow = 'OutputFillClosedArrow', } +/** + * Extends Sprotty's SLabel to add support for having the initial text when entering + * in direct edit mode different from the text's label itself, and makes the + * pre-selection of the edited text optional. + */ export class Label extends SLabel { + isMultiLine: boolean = true; style: LabelStyle; + initialText: string; + preSelect: boolean = true; } export class LabelStyle { diff --git a/frontend/src/diagram/sprotty/DiagramServer.tsx b/frontend/src/diagram/sprotty/DiagramServer.tsx index c0488e3f32..fa036aa768 100644 --- a/frontend/src/diagram/sprotty/DiagramServer.tsx +++ b/frontend/src/diagram/sprotty/DiagramServer.tsx @@ -19,7 +19,7 @@ import { Tool, } from 'diagram/DiagramWebSocketContainer.types'; import { convertDiagram } from 'diagram/sprotty/convertDiagram'; -import { SEditableLabel } from 'diagram/sprotty/DependencyInjection'; +import { Label } from 'diagram/sprotty/Diagram.types'; import { SetActiveConnectorToolsAction, ShowContextualMenuAction, @@ -251,7 +251,7 @@ export class DiagramServer extends ModelSource { selectedItems.forEach((item) => { const label = item.editableLabel; if (label) { - const editableLabel = item.children.find((c) => c instanceof SEditableLabel); + const editableLabel = item.children.find((c) => c instanceof Label); if (editableLabel && action.initialText) { editableLabel.initialText = action.initialText; } diff --git a/frontend/src/diagram/sprotty/EditLabelUIWithInitialContent.ts b/frontend/src/diagram/sprotty/EditLabelUIWithInitialContent.ts new file mode 100644 index 0000000000..84c065c208 --- /dev/null +++ b/frontend/src/diagram/sprotty/EditLabelUIWithInitialContent.ts @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2022 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +import { Label } from 'diagram/sprotty/Diagram.types'; +import { EditLabelUI, getAbsoluteClientBounds, getZoom } from 'sprotty'; + +export class EditLabelUIWithInitialContent extends EditLabelUI { + protected applyTextContents() { + if (this.label instanceof Label) { + this.editControl.value = this.label.initialText || this.label.text; + if (this.label.preSelect) { + this.editControl.setSelectionRange(0, this.editControl.value.length); + } + } else { + super.applyTextContents(); + } + } + + /** + * Overriden to have the same editing area size and to center it with the edited label + */ + protected setPosition(containerElement: HTMLElement) { + let x = 0; + let y = 0; + let width = 100; + let height = 20; + // used to avoid the scrollbar + const extraSize: number = 10; + + if (this.label) { + const nbLines: number = this.label.text.split('\n').length; + const zoom = getZoom(this.label); + const bounds = getAbsoluteClientBounds(this.label, this.domHelper, this.viewerOptions); + // make the edit area centered on the label + x = bounds.x + (bounds.width * (1 - 1 / zoom)) / 2; + y = bounds.y; + height = height * nbLines + extraSize; + width = bounds.width / zoom + extraSize; + } + + containerElement.style.left = `${x}px`; + containerElement.style.top = `${y}px`; + containerElement.style.width = `${width}px`; + this.editControl.style.width = `${width}px`; + containerElement.style.height = `${height}px`; + this.editControl.style.height = `${height}px`; + } + + /** + * Overriden to keep the same font size whatever the zoom and to center the text + */ + protected applyFontStyling() { + // super.applyFontStyling(); + if (this.label) { + this.labelElement = document.getElementById(this.domHelper.createUniqueDOMElementId(this.label)); + if (this.labelElement) { + this.labelElement.style.visibility = 'hidden'; + const style = window.getComputedStyle(this.labelElement); + this.editControl.style.font = style.font; + this.editControl.style.fontStyle = style.fontStyle; + this.editControl.style.fontFamily = style.fontFamily; + this.editControl.style.fontWeight = style.fontWeight; + this.editControl.style.textAlign = 'center'; + } + } + } +} diff --git a/frontend/src/diagram/sprotty/views/LabelView.tsx b/frontend/src/diagram/sprotty/views/LabelView.tsx index c88768680d..f3e790db40 100644 --- a/frontend/src/diagram/sprotty/views/LabelView.tsx +++ b/frontend/src/diagram/sprotty/views/LabelView.tsx @@ -14,6 +14,32 @@ import { setAttr, SLabelView, svg } from 'sprotty'; import { getSubType } from 'sprotty-protocol'; +const Text = (props) => { + const { attrs } = props; + const { text, fontSize } = attrs; + + const lines = text.split('\n'); + if (lines.length == 1) { + return text; + } else { + return lines.map((line, index) => { + if (index === 0) { + return {line}; + } else { + if (line.length == 0) { + // avoid tspan to be ignored if there is only a line return + line = ' '; //$NON-NLS-1$ + } + return ( + + {line} + + ); + } + }); + } +}; + /** * The view used to display labels. * @@ -31,6 +57,7 @@ export class LabelView extends SLabelView { 'font-weight': 'normal', 'font-style': 'normal', 'text-decoration': 'none', + 'text-anchor': 'start', }; if (bold) { styleObject['font-weight'] = 'bold'; @@ -48,12 +75,18 @@ export class LabelView extends SLabelView { styleObject['text-decoration'] += ' line-through'; } } + if (label.type.includes('center')) { + styleObject['text-anchor'] = 'middle'; + } const iconVerticalOffset = -12; + + const text = label.text; + const vnode = ( {iconURL ? : ''} - {label.text} + ); diff --git a/frontend/src/properties/propertysections/TextfieldPropertySection.tsx b/frontend/src/properties/propertysections/TextfieldPropertySection.tsx index 91c7e3ab34..c03b39e55d 100644 --- a/frontend/src/properties/propertysections/TextfieldPropertySection.tsx +++ b/frontend/src/properties/propertysections/TextfieldPropertySection.tsx @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2021 Obeo. + * Copyright (c) 2019, 2022 Obeo. * This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -185,7 +185,7 @@ export const TextfieldPropertySection = ({ }; const onKeyPress = (event) => { - if ('Enter' === event.key) { + if ('Enter' === event.key && !event.shiftKey) { sendEditedValue(); } };