diff --git a/.gitignore b/.gitignore index 0d379eafe..27238de7a 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,8 @@ fabric.properties /use-assembly/target **/.DS_Store + +# ArchUnit generated failure reports (the directory is kept via its README, +# the report files are regenerated on every test run) +/docs/archunit-results/* +!/docs/archunit-results/README diff --git a/NEWS b/NEWS index 95c0f3365..6bc587bcf 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,8 @@ Please see the file `README' for a description of how to report bugs. ** Changes between version 7.1.0 and X.X.X * USE now supports data types +* [GUI] The class diagram option "Group multiplicities / role names" is now + saved in the layout file and is enabled by default for class diagrams (#16) ** Changes between version 5.2.0 and 6.0.0 * New OCL complexity plugin diff --git a/use-gui/src/main/java/org/tzi/use/gui/views/diagrams/DiagramOptions.java b/use-gui/src/main/java/org/tzi/use/gui/views/diagrams/DiagramOptions.java index 344de5cd9..a81ff36f2 100644 --- a/use-gui/src/main/java/org/tzi/use/gui/views/diagrams/DiagramOptions.java +++ b/use-gui/src/main/java/org/tzi/use/gui/views/diagrams/DiagramOptions.java @@ -38,7 +38,7 @@ public abstract class DiagramOptions { /** * The current version number of the layout information */ - public static int XML_LAYOUT_VERSION = 13; + public static int XML_LAYOUT_VERSION = 14; protected boolean fDoAutoLayout = false; protected boolean fSaveDefaultLayout = true; @@ -295,6 +295,7 @@ public void saveOptions(PersistHelper helper, Element parent) { helper.appendChild(parent, LayoutTags.SHOWOPERATIONS, String.valueOf(isShowOperations())); helper.appendChild(parent, LayoutTags.SHOWROLENAMES, String.valueOf(isShowRolenames())); helper.appendChild(parent, LayoutTags.SHOWGRID, String.valueOf(showGrid())); + helper.appendChild(parent, LayoutTags.GROUPMR, String.valueOf(isGroupMR())); } /** * @param rootElement @@ -310,6 +311,9 @@ public void loadOptions(PersistHelper helper, int version) { if (version > 1) { setShowGrid(helper.getElementBooleanValue(LayoutTags.SHOWGRID)); } + if (version > 13) { + setGroupMR(helper.getElementBooleanValue(LayoutTags.GROUPMR)); + } } public void addOptionChangedListener(DiagramOptionChangedListener listener) { diff --git a/use-gui/src/main/java/org/tzi/use/gui/views/diagrams/DiagramView.java b/use-gui/src/main/java/org/tzi/use/gui/views/diagrams/DiagramView.java index 4685e0f93..d9035ad1b 100644 --- a/use-gui/src/main/java/org/tzi/use/gui/views/diagrams/DiagramView.java +++ b/use-gui/src/main/java/org/tzi/use/gui/views/diagrams/DiagramView.java @@ -1070,13 +1070,18 @@ public void loadLayoutFromString(String layoutString) { version = Integer.valueOf(helper.getAttributeValue("version")); if (beforeRestorePlacementInfos(version)) { - helper.toFirstChild("diagramOptions"); - this.getOptions().loadOptions(helper, version); - helper.toParent(); - - helper.setAllNodes(this.collectAllNodes()); - this.restorePlacementInfos(helper, version); - this.invalidateContent(false); + this.getOptions().setIsLoadingLayout(true); + try { + helper.toFirstChild("diagramOptions"); + this.getOptions().loadOptions(helper, version); + helper.toParent(); + + helper.setAllNodes(this.collectAllNodes()); + this.restorePlacementInfos(helper, version); + this.invalidateContent(false); + } finally { + this.getOptions().setIsLoadingLayout(false); + } } this.repaint(); @@ -1095,15 +1100,20 @@ public void loadLayout(Path layoutFile) { version = Integer.valueOf(helper.getAttributeValue("version")); if (beforeRestorePlacementInfos(version)) { - helper.toFirstChild("diagramOptions"); - this.getOptions().loadOptions(helper, version); - helper.toParent(); + this.getOptions().setIsLoadingLayout(true); + try { + helper.toFirstChild("diagramOptions"); + this.getOptions().loadOptions(helper, version); + helper.toParent(); - helper.setAllNodes(this.collectAllNodes()); - this.restorePlacementInfos(helper, version); - this.invalidateContent(false); + helper.setAllNodes(this.collectAllNodes()); + this.restorePlacementInfos(helper, version); + this.invalidateContent(false); - afterLoadLayout(layoutFile); + afterLoadLayout(layoutFile); + } finally { + this.getOptions().setIsLoadingLayout(false); + } } this.repaint(); diff --git a/use-gui/src/main/java/org/tzi/use/gui/views/diagrams/classdiagram/ClassDiagramOptions.java b/use-gui/src/main/java/org/tzi/use/gui/views/diagrams/classdiagram/ClassDiagramOptions.java index e8d98713f..a125b6833 100644 --- a/use-gui/src/main/java/org/tzi/use/gui/views/diagrams/classdiagram/ClassDiagramOptions.java +++ b/use-gui/src/main/java/org/tzi/use/gui/views/diagrams/classdiagram/ClassDiagramOptions.java @@ -53,6 +53,11 @@ public String toString() { } public ClassDiagramOptions() { + // In class diagrams, group multiplicities and role names by default + // (issue #16) so that their positions can be adjusted together. The + // value is persisted with the layout, so an explicit user choice + // overrides this default when a layout is loaded. + this.fGroupMR = true; } protected ShowCoverage fShowCoverage = ShowCoverage.DONT_SHOW; diff --git a/use-gui/src/main/java/org/tzi/use/gui/views/diagrams/elements/MultiplicityRolenameWrapper.java b/use-gui/src/main/java/org/tzi/use/gui/views/diagrams/elements/MultiplicityRolenameWrapper.java index bca9e0456..e8daf947a 100644 --- a/use-gui/src/main/java/org/tzi/use/gui/views/diagrams/elements/MultiplicityRolenameWrapper.java +++ b/use-gui/src/main/java/org/tzi/use/gui/views/diagrams/elements/MultiplicityRolenameWrapper.java @@ -43,6 +43,8 @@ public final class MultiplicityRolenameWrapper implements PositionChangedListener position_changed_listener = null; + private final DiagramOptions options; + private boolean do_group = false; double offset = 0; @@ -51,6 +53,7 @@ public MultiplicityRolenameWrapper(Multiplicity multiplicity_client, Rolename rolename_client, PropertyOwner end, DiagramOptions options) { this.multiplicity_client = multiplicity_client; this.rolename_client = rolename_client; + this.options = options; // this.end = end; @@ -64,18 +67,35 @@ public MultiplicityRolenameWrapper(Multiplicity multiplicity_client, // Let this wrapper listen to position changes of either multiplicity or // role name nodes. instantiatePositionChangedListener(); + + // Reflect the current grouping option (which may have been restored + // from a layout file or set to its default) right from the start. + do_group = options.isGroupMR(); + if (do_group) { + attach_listener(); + } } @Override public void optionChanged(String optionname) { - if (optionname.equals("GROUPMR")) - do_group = !do_group; - - if (do_group) + if (!optionname.equals("GROUPMR")) { + return; + } + + // Mirror the actual option value instead of blindly toggling. This + // keeps the grouping state in sync with the option even when the value + // is set explicitly, e.g. while loading a persisted layout. + boolean group = options.isGroupMR(); + if (group == do_group) { + return; + } + + do_group = group; + if (do_group) { attach_listener(); - else + } else { detach_listener(); - + } } protected void determine_offset() { @@ -91,6 +111,14 @@ protected void instantiatePositionChangedListener() { public void positionChanged(Object source, Point2D currentPosition, double deltaX, double deltaY) { + // While a layout is being restored, the stored positions are + // applied directly. Grouping must not drag the partner node + // during restore, otherwise the persisted positions of + // multiplicities / role names would be overwritten. + if (options.isLoadingLayout()) { + return; + } + detach_listener(); if (source instanceof Multiplicity) diff --git a/use-gui/src/main/java/org/tzi/use/gui/xmlparser/LayoutTags.java b/use-gui/src/main/java/org/tzi/use/gui/xmlparser/LayoutTags.java index e5bc25028..df91494ce 100644 --- a/use-gui/src/main/java/org/tzi/use/gui/xmlparser/LayoutTags.java +++ b/use-gui/src/main/java/org/tzi/use/gui/xmlparser/LayoutTags.java @@ -36,6 +36,7 @@ public class LayoutTags { public static final String SHOWATTRIBUTES = "showattributes"; public static final String SHOWOPERATIONS = "showoperations"; public static final String SHOWGRID = "showgrid"; + public static final String GROUPMR = "groupmultiplicityrolenames"; // node and edge tags public static final String NAME = "name"; diff --git a/use-gui/src/test/java/org/tzi/use/gui/views/diagrams/DiagramOptionsPersistenceTest.java b/use-gui/src/test/java/org/tzi/use/gui/views/diagrams/DiagramOptionsPersistenceTest.java new file mode 100644 index 000000000..aa949768e --- /dev/null +++ b/use-gui/src/test/java/org/tzi/use/gui/views/diagrams/DiagramOptionsPersistenceTest.java @@ -0,0 +1,251 @@ +/* + * USE - UML based specification environment + * Copyright (C) 1999-2026 University of Bremen & University of Applied Sciences Hamburg + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +package org.tzi.use.gui.views.diagrams; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.junit.jupiter.api.Test; +import org.tzi.use.gui.util.PersistHelper; +import org.tzi.use.gui.views.diagrams.classdiagram.ClassDiagramOptions; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Tests that the diagram layout options are correctly persisted to and restored + * from the layout file. + * + *
These tests focus on the option "Group multiplicities / role names" + * (see {@link DiagramOptions#isGroupMR()}), which was reported to not be + * persisted in the layout file (issue #16). The tests exercise the exact + * save/load path used by + * {@link DiagramView#saveLayoutAsString()} / + * {@link DiagramView#loadLayoutFromString(String)}: the options are written + * into a {@code diagramOptions} element of a versioned {@code diagram_Layout} + * document and read back through the same navigation.
+ * + * @author Cansin Yildiz + * @author Claude + */ +public class DiagramOptionsPersistenceTest { + + /** + * The XML element name expected for the "Group multiplicities / role names" + * option in the persisted layout file. + */ + private static final String GROUP_MR_TAG = "groupmultiplicityrolenames"; + + /** + * Serializes the options of {@code options} into a layout XML document, + * exactly as {@link DiagramView} does when saving a layout, and returns + * the resulting bytes. + * + * @param options the options to persist + * @param version the layout version to write into the {@code diagram_Layout} root + */ + private byte[] saveOptions(DiagramOptions options, int version) throws Exception { + DocumentBuilderFactory fact = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = fact.newDocumentBuilder(); + Document doc = docBuilder.newDocument(); + + PersistHelper writeHelper = new PersistHelper(new PrintWriter(new StringWriter())); + + Element rootElement = doc.createElement("diagram_Layout"); + rootElement.setAttribute("version", String.valueOf(version)); + doc.appendChild(rootElement); + + Element optionsElement = doc.createElement("diagramOptions"); + rootElement.appendChild(optionsElement); + options.saveOptions(writeHelper, optionsElement); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.transform(new DOMSource(doc), new StreamResult(bos)); + return bos.toByteArray(); + } + + /** + * Restores the diagram options from a layout XML document into + * {@code options}, exactly as {@link DiagramView} does when loading a layout. + */ + private void loadOptions(byte[] layoutXml, DiagramOptions options) throws Exception { + PersistHelper readHelper = new PersistHelper(layoutXml, new PrintWriter(new StringWriter())); + + int version = 1; + if (readHelper.hasAttribute("version")) { + version = Integer.valueOf(readHelper.getAttributeValue("version")); + } + + readHelper.toFirstChild("diagramOptions"); + options.loadOptions(readHelper, version); + readHelper.toParent(); + } + + /** + * Convenience overload that saves with the current layout version. + */ + private byte[] saveOptions(DiagramOptions options) throws Exception { + return saveOptions(options, DiagramOptions.XML_LAYOUT_VERSION); + } + + /** + * Issue #16: an enabled "Group multiplicities / role names" option must + * survive a save/load round-trip. + */ + @Test + public void groupMultiplicityRolenameTrueSurvivesRoundTrip() throws Exception { + ClassDiagramOptions saved = new ClassDiagramOptions(); + saved.setGroupMR(true); + + byte[] layoutXml = saveOptions(saved); + + ClassDiagramOptions loaded = new ClassDiagramOptions(); + // Force the opposite value first, so a passing assertion proves the + // value was really restored from the file (not left at a default). + loaded.setGroupMR(false); + loadOptions(layoutXml, loaded); + + assertTrue(loaded.isGroupMR(), + "Group multiplicities/role names = true must be persisted and restored"); + } + + /** + * Issue #16: a disabled "Group multiplicities / role names" option must + * survive a save/load round-trip as well (the user's explicit choice must + * be honored, not silently reset to the default). + */ + @Test + public void groupMultiplicityRolenameFalseSurvivesRoundTrip() throws Exception { + ClassDiagramOptions saved = new ClassDiagramOptions(); + saved.setGroupMR(false); + + byte[] layoutXml = saveOptions(saved); + + ClassDiagramOptions loaded = new ClassDiagramOptions(); + // Force the opposite value first. + loaded.setGroupMR(true); + loadOptions(layoutXml, loaded); + + assertFalse(loaded.isGroupMR(), + "Group multiplicities/role names = false must be persisted and restored"); + } + + /** + * The saved layout file must actually contain an element for the + * "Group multiplicities / role names" option. + */ + @Test + public void savedLayoutContainsGroupMultiplicityRolenameElement() throws Exception { + ClassDiagramOptions options = new ClassDiagramOptions(); + options.setGroupMR(true); + + String layoutXml = new String(saveOptions(options), StandardCharsets.UTF_8); + + assertTrue(layoutXml.contains("<" + GROUP_MR_TAG + ">true" + GROUP_MR_TAG + ">"), + "Saved layout must contain the group-multiplicities/role-names option element, but was:\n" + + layoutXml); + } + + /** + * Issue #16 (requested default): a brand-new class diagram should group + * multiplicities and role names by default. + */ + @Test + public void groupMultiplicityRolenameDefaultsToTrue() { + assertTrue(new ClassDiagramOptions().isGroupMR(), + "New class diagrams should group multiplicities/role names by default"); + } + + /** + * Adding the new option requires bumping the layout version so that the + * loader can tell old layout files (without the option) from new ones. + */ + @Test + public void layoutVersionWasBumpedForNewOption() { + assertTrue(DiagramOptions.XML_LAYOUT_VERSION >= 14, + "The layout version must be increased to at least 14 for the new option"); + } + + /** + * Backward compatibility: loading a pre-v14 layout file (which does not + * contain the option) must not fail and must not silently turn the option + * off. The default must be kept, guarded by the layout version. + */ + @Test + public void loadingPreVersion14LayoutKeepsDefault() throws Exception { + String oldLayout = + "\n" + + "