mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-25 19:34:13 +02:00
84add7b3ef
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 "<a> </a>" 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 <noreply@anthropic.com>
276 lines
7.7 KiB
C++
276 lines
7.7 KiB
C++
/*
|
|
Copyright 2006-2026 The QElectroTech Team
|
|
This file is part of QElectroTech.
|
|
|
|
QElectroTech 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.
|
|
|
|
QElectroTech 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 QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "diagramcontext.h"
|
|
#include "qet.h"
|
|
#include <QDebug>
|
|
#include <QRegularExpression>
|
|
#include <QRegularExpressionMatch>
|
|
|
|
/**
|
|
@brief DiagramContext::add
|
|
Add all value of other to this.
|
|
If a key already exist, the value is replaced.
|
|
If a key doesn't exist, it will be added.
|
|
All other keys of this context, which are not present in other, stay unchanged.
|
|
@param other
|
|
*/
|
|
void DiagramContext::add(DiagramContext other)
|
|
{
|
|
for (QString key : other.keys()) {
|
|
addValue(key, other.value(key));
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief DiagramContext::remove
|
|
@param key
|
|
*/
|
|
void DiagramContext::remove(const QString &key) {
|
|
m_content.remove(key);
|
|
}
|
|
|
|
/**
|
|
@return a list containing all the keys in the context object.
|
|
*/
|
|
QList<QString> DiagramContext::keys(DiagramContext::KeyOrder order) const
|
|
{
|
|
if (order == None) {
|
|
return m_content.keys();
|
|
}
|
|
else
|
|
{
|
|
QList<QString> keys_list = m_content.keys();
|
|
if (order == Alphabetical) {
|
|
std::sort(keys_list.begin(), keys_list.end());
|
|
} else {
|
|
std::sort(keys_list.begin(), keys_list.end(), DiagramContext::stringLongerThan);
|
|
}
|
|
return(keys_list);
|
|
}
|
|
}
|
|
|
|
/**
|
|
@param key string key
|
|
@return true if that key is known to the diagram context, false otherwise
|
|
*/
|
|
bool DiagramContext::contains(const QString &key) const
|
|
{
|
|
return(m_content.contains(key));
|
|
}
|
|
|
|
/**
|
|
@param key
|
|
*/
|
|
const QVariant DiagramContext::operator[](const QString &key) const
|
|
{
|
|
return(m_content[key]);
|
|
}
|
|
|
|
/**
|
|
@param key key to insert in the context - the key may only contain lowercase
|
|
letters and dashes.
|
|
If embedded key is set, key must be find it else value is not added.
|
|
@see DiagramContext::keyIsAcceptable()
|
|
@param value value to insert in the context
|
|
@param show if value is used to be show on the diagram or somewhere else,
|
|
we can specify if he is show(true) or not(false)
|
|
@return true if the insertion succeeds, false otherwise
|
|
*/
|
|
bool DiagramContext::addValue(const QString &key, const QVariant &value, bool show) {
|
|
if (isKeyAcceptable(key)) {
|
|
m_content.insert(key, value);
|
|
m_content_show.insert(key, show);
|
|
return(true);
|
|
}
|
|
return(false);
|
|
}
|
|
|
|
QVariant DiagramContext::value(const QString &key) const
|
|
{
|
|
return m_content.value(key);
|
|
}
|
|
|
|
/**
|
|
Clear the content of this diagram context.
|
|
*/
|
|
void DiagramContext::clear()
|
|
{
|
|
m_content.clear();
|
|
m_content_show.clear();
|
|
}
|
|
|
|
/**
|
|
@return the number of key/value pairs stored in this object.
|
|
*/
|
|
int DiagramContext::count()
|
|
{
|
|
return(m_content.count());
|
|
}
|
|
|
|
/**
|
|
@brief DiagramContext::keyMustShow
|
|
@return the value pairs with key, if key not found, return false
|
|
*/
|
|
bool DiagramContext::keyMustShow(const QString &key) const
|
|
{
|
|
if (m_content_show.contains(key))
|
|
return m_content_show[key];
|
|
return false;
|
|
}
|
|
|
|
bool DiagramContext::operator==(const DiagramContext &dc) const
|
|
{
|
|
return(m_content == dc.m_content &&
|
|
m_content_show == dc.m_content_show);
|
|
}
|
|
|
|
bool DiagramContext::operator!=(const DiagramContext &dc) const
|
|
{
|
|
return(!(*this == dc));
|
|
}
|
|
|
|
/**
|
|
Export this context properties under the \a e XML element, using tags
|
|
named \a tag_name (defaults to "property").
|
|
*/
|
|
void DiagramContext::toXml(QDomElement &e, const QString &tag_name) const
|
|
{
|
|
foreach (QString key, keys()) {
|
|
const QString raw = m_content[key].toString();
|
|
if ((tag_name == "elementInformation") && raw.trimmed().isEmpty()) {
|
|
continue;
|
|
}
|
|
QDomElement property = e.ownerDocument().createElement(tag_name);
|
|
// try to sort attributes by removing and re-adding
|
|
property.removeAttribute("show");
|
|
property.removeAttribute("name");
|
|
property.setAttribute("show", m_content_show[key]);
|
|
property.setAttribute("name", key);
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
Read this context properties from the \a e XML element, looking for tags named
|
|
\a tag_name (defaults to "property").
|
|
*/
|
|
void DiagramContext::fromXml(const QDomElement &e, const QString &tag_name) {
|
|
foreach (QDomElement property, QET::findInDomElement(e, tag_name)) {
|
|
if (!property.hasAttribute("name")) continue;
|
|
addValue(property.attribute("name"), QVariant(property.text()));
|
|
m_content_show.insert(property.attribute("name"), property.attribute("show", "1").toInt());
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief DiagramContext::fromXml
|
|
Read this context properties from the dom_element,
|
|
looking for tags named tag_name
|
|
@param dom_element : dom element to parse
|
|
@param tag_name : tag name to find, by default "property"
|
|
*/
|
|
void DiagramContext::fromXml(const pugi::xml_node &dom_element, const QString &tag_name)
|
|
{
|
|
for(auto node = dom_element.child(tag_name.toStdString().c_str()) ; node ; node = node.next_sibling(tag_name.toStdString().c_str()))
|
|
{
|
|
addValue(node.attribute("name").as_string(), QVariant(node.text().as_string()));
|
|
m_content_show.insert(node.attribute("name").as_string(), node.attribute("show").empty()? 1 : node.attribute("show").as_int());
|
|
}
|
|
}
|
|
|
|
/**
|
|
Export this context properties to \a settings by creating an array named \a
|
|
array_name.
|
|
*/
|
|
void DiagramContext::toSettings(QSettings &settings, const QString &array_name) const
|
|
{
|
|
settings.beginWriteArray(array_name);
|
|
int i = 0;
|
|
foreach (QString key, m_content.keys()) {
|
|
settings.setArrayIndex(i);
|
|
settings.setValue("name", key);
|
|
settings.setValue("value", m_content[key].toString());
|
|
++ i;
|
|
}
|
|
settings.endArray();
|
|
}
|
|
|
|
/**
|
|
Read this context properties from \a settings by running through the array
|
|
named \a array_name.
|
|
*/
|
|
void DiagramContext::fromSettings(QSettings &settings, const QString &array_name) {
|
|
int size = settings.beginReadArray(array_name);
|
|
for (int i = 0 ; i < size; ++ i) {
|
|
settings.setArrayIndex(i);
|
|
QString key = settings.value("name").toString();
|
|
if (key.isEmpty()) continue;
|
|
addValue(key, settings.value("value").toString());
|
|
}
|
|
settings.endArray();
|
|
}
|
|
|
|
/**
|
|
@return the regular expression used to check whether a given key is acceptable.
|
|
@see keyIsAcceptable()
|
|
*/
|
|
QString DiagramContext::validKeyRegExp()
|
|
{
|
|
return("^[a-z0-9-_]+$");
|
|
}
|
|
|
|
/**
|
|
@return True if \a a is longer than \a b, false otherwise.
|
|
*/
|
|
bool DiagramContext::stringLongerThan(const QString &a, const QString &b) {
|
|
return (a.length() > b.length());
|
|
}
|
|
|
|
/**
|
|
@param key a key string
|
|
@return true if that key is acceptable, false otherwise
|
|
*/
|
|
bool DiagramContext::isKeyAcceptable(const QString &key)
|
|
{
|
|
static const QRegularExpression re(DiagramContext::validKeyRegExp());
|
|
|
|
QRegularExpressionMatch match = re.match(key);
|
|
return match.hasMatch();
|
|
}
|
|
|
|
QDebug operator <<(QDebug debug, const DiagramContext &context)
|
|
{
|
|
debug << "DiagramContext";
|
|
for (auto key : context.keys()) {
|
|
debug.nospace() << key << " : " << context.value(key) << "\n";
|
|
}
|
|
debug << " end DiagramContext";
|
|
|
|
return debug;
|
|
}
|