Files
qelectrotech-source-mirror/sources/diagramcontext.cpp
T
ispyisail 75450d7102 Make saving a just-saved project change nothing
Saving a project that had just been saved changed it again in 18 of the
24 example projects, so a project kept in version control showed changes
nobody made. Both causes were cleanup done on save but not on load:

- Symbol information whose values were all empty was written as an empty
  <elementInformations/> block (DiagramContext::toXml() skips empty
  values, Element::toXml() wrote the block anyway). The next load read it
  as no information and the next save dropped it. The block is now written
  only when something went into it.
- Information values were trimmed on save but not on load, so a label with
  stray spaces (" PRISE") kept them in memory and in its displayed copy
  until the project was opened again. The same rule, kept in one place,
  now applies when reading: stray whitespace around real content trimmed,
  a value that is only whitespace kept (#973).

All 24 examples now save identically a second time (master: 6), and each
one's first save is byte-for-byte what master wrote only on its second.
A title-block property set to a single space keeps it through two saves.

tst_resaveunchanged runs --resave twice on Projet_vierge.qet and
m_000.qet; both fail without this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:59:02 +13:00

290 lines
8.2 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));
}
namespace {
/**
The value as it is saved, and so as it is read back: stray leading and
trailing whitespace around real content trimmed, 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.
Applied when reading as well as when writing, so that what is in memory
after a load is what the next save writes: a label shown from an
untrimmed value would otherwise keep its spaces on screen and in its
displayed copy until the project was saved and opened again, and a
just-saved project would change on its second save.
*/
QString storedValue(const QString &raw)
{
return raw.trimmed().isEmpty() ? raw : raw.trimmed();
}
} // namespace
/**
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);
QDomText value = e.ownerDocument().createTextNode(storedValue(raw));
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(storedValue(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(storedValue(QString::fromUtf8(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;
}