From 84add7b3ef7b0900e61f6754651d1ab6745f4181 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 23 Sep 2026 09:20:31 +1200 Subject: [PATCH] Preserve an all-whitespace context value through save and reload A property value that is entirely whitespace -- reported in #973 as a workaround (setting a title-block custom variable to a single space, the only way to give it a value other than blank before that bug was fixed in #989) -- did not survive a save/reload cycle. Two independent causes, both needed for the round trip to actually work: 1. DiagramContext::toXml() called .trimmed() on every stored value before writing it, unconditionally. For ordinary content this only strips accidental leading/trailing whitespace, but for a value that IS whitespace it collapses the entire thing to "", indistinguishable from a value that was never set. 2. QDomDocument::setContent(), used to parse the project file, discards a text node that is entirely whitespace by default. Confirmed in isolation, outside any QET code: parsing " " with the default ParseOptions gives QDomElement::text() == ""; adding ParseOption::PreserveSpacingOnlyNodes gives " ". So even once (1) stops destroying the value on save, the very next load throws it away again. Fix (1) only trims when the trimmed result isn't empty, i.e. leaves an all-whitespace value untouched. Fix (2) adds PreserveSpacingOnlyNodes to the one setContent() call that parses a project file (QETProject::readProjectXml()) -- not the other ~19 call sites in the codebase (clipboard paste, element/macro loading, translations, autonum context), which read different, narrower documents and are not implicated in this report. Blast radius of (2): every place that walks a QDomNode's children already filters on isElement() (see QET::findInDomElement()), so the extra whitespace-only text-node siblings this keeps around are inert wherever current code already expected only elements. The one place it isn't inert is exactly the bug -- calling .text() on an element whose entire content is whitespace. Verified end-to-end, not just at one stage: a single-space title-block variable now survives two successive --resave cycles unchanged (confirmed byte-for-byte in the saved XML), and renders as blank space rather than literal placeholder text or a vanished value. Re-saved all 24 shipped examples with and without this change and diffed: 23 byte-identical, the one that differs (schema_indus.qet) differs only in element uuids -- and resaving it twice with the SAME unpatched binary produces that same kind of diff, confirming it is pre-existing non-determinism in files that predate persisted uuids, unrelated to this change. Qt 6.10.2, ctest 11/11. Co-Authored-By: Claude Sonnet 5 --- sources/diagramcontext.cpp | 14 +++++++++++--- sources/qetproject.cpp | 13 ++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/sources/diagramcontext.cpp b/sources/diagramcontext.cpp index 4e4f0d205..5137a7bf5 100644 --- a/sources/diagramcontext.cpp +++ b/sources/diagramcontext.cpp @@ -151,8 +151,8 @@ bool DiagramContext::operator!=(const DiagramContext &dc) const void DiagramContext::toXml(QDomElement &e, const QString &tag_name) const { foreach (QString key, keys()) { - if ((tag_name == "elementInformation") && - (m_content[key].toString().trimmed().isEmpty())) { + const QString raw = m_content[key].toString(); + if ((tag_name == "elementInformation") && raw.trimmed().isEmpty()) { continue; } QDomElement property = e.ownerDocument().createElement(tag_name); @@ -161,7 +161,15 @@ void DiagramContext::toXml(QDomElement &e, const QString &tag_name) const property.removeAttribute("name"); property.setAttribute("show", m_content_show[key]); property.setAttribute("name", key); - QDomText value = e.ownerDocument().createTextNode(m_content[key].toString().trimmed()); + // Trim stray leading/trailing whitespace around real content, but + // not a value that IS whitespace: unconditionally trimming an + // all-whitespace string collapses it to "", which is silently + // indistinguishable from a value that was never set. A title-block + // custom variable set to a single space -- a workaround for #973, + // where an unset variable renders as its own literal placeholder -- + // would otherwise vanish on the very next save. + const QString stored = raw.trimmed().isEmpty() ? raw : raw.trimmed(); + QDomText value = e.ownerDocument().createTextNode(stored); property.appendChild(value); e.appendChild(property); } diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index 49b4a6520..af0f00b25 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -334,7 +334,18 @@ QETProject::ProjectState QETProject::openFile(QFile *file) //file without a persisted uuid derives its uuid from them. const QByteArray content = file->readAll(); QDomDocument xml_project; - if (!xml_project.setContent(content)) + // PreserveSpacingOnlyNodes: without it, a text node that is entirely + // whitespace -- e.g. a title-block custom variable deliberately set to + // a single space, the only way to give it a value other than blank + // (bugtracker #973) -- is silently dropped by Qt's default parsing, + // and QDomElement::text() then returns "" for it exactly as if it had + // never been set. Confirmed in isolation: parses to text()=="", + // this option makes it text()==" ". Every place in this codebase that + // walks a QDomNode's children already filters on isElement() (see + // QET::findInDomElement()), so the extra whitespace-only text nodes + // this keeps around are inert everywhere but the two elements that + // call .text() on themselves -- which is exactly where the bug was. + if (!xml_project.setContent(content, QDomDocument::ParseOption::PreserveSpacingOnlyNodes)) { if(opened_here) { file->close();