Change a symbol's information without building its folio (stage D5)

A script changing the information of a symbol on a folio not built, when
nothing else shows that information (no symbol built holds it, no
partner is linked to it, no label formula), changes it through the
project database: the store and the symbol's element info row, undoable
by uuid (ChangeStoredElementInformationCommand). Reading it back reads
the store. Building the symbol later takes the change
(projectDataBase::takeUnbuiltEdit(), in Element::fromXml()), the label
and texts following as for any change. Saving a .qetz writes the
changed information in the database in place of the rows the folio was
read with, its file copied, the folio still not built; a symbol whose
file holds no information block, or a .qet, builds the folio to save.

136-folio .qetz, "comment" set on the 3 513 symbols the database can
change: 0.9 s, no folio built, saved in 6.1 s, none built (27 s and 116
folios when saving built them). The same edits on the 4 039 symbols with
no partner and no formula, lazily and with every folio built: the two
saved projects are the same file. tst_lazyfolios: a script changes a
symbol not built, building none; saved as when built (fails when
building ignores the change, and when the .qetz writer does); undone;
the folio built afterwards shows it. ctest 96/96 eager and lazy; 342
corpus projects save the same in all four arms and export the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-12 16:36:56 +13:00
parent 9078e4e321
commit 244de05421
10 changed files with 345 additions and 3 deletions
+22
View File
@@ -749,6 +749,28 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error, const QB
QStringLiteral("INSERT INTO conductor SELECT * FROM old.conductor WHERE folio_id = ? ORDER BY rowid")}) {
Statement rows(db, sql);
ok = ok && rows.run({id});
}
//Its symbols whose information changed since (QETProject::toXml()):
//their information rows written as a symbol's block writes them
for (const QDomElement &edited : children(d, QStringLiteral("qetz-edited-element"))) {
Statement find(db, QStringLiteral("SELECT key FROM element WHERE folio_id = ? AND uuid = ? AND info_at IS NOT NULL"));
find.bind({id, edited.attribute(QStringLiteral("uuid"))});
if (!find.next()) {
setError(error, QStringLiteral("a symbol changed on folio %1 kept as it was is not in its database").arg(id));
return {};
}
const QString key = find.value(0).toString();
Statement drop(db, QStringLiteral("DELETE FROM element_info WHERE element_key = ?"));
ok = ok && drop.run({key});
int ord = 0;
for (QDomElement i : children(edited.firstChildElement(QStringLiteral("elementInformations")),
QStringLiteral("elementInformation"))) {
QVariant value;
simpleValue(i, &value);
ok = ok && element_info.run({key, ord++, i.hasAttribute(QStringLiteral("name"))
? QVariant(i.attribute(QStringLiteral("name"))) : QVariant(),
value, attributesJson(i, QStringLiteral("name"), true)});
}
}
if (!ok) {
setError(error, QStringLiteral("folio %1 kept as it was is not in its database").arg(id));
+140
View File
@@ -1378,6 +1378,146 @@ autonum::sequentialNumbers projectDataBase::sequenceFromValue(const QVariant &va
return sequence;
}
/**
@brief projectDataBase::canEditUnbuilt
@return true if the information of the symbol @p element can be changed
through the store alone, its folio not built (QET_LAZY_FOLIOS): it is
placed once, on a folio not built, and nothing else shows its
information -- no symbol built (alone or with its folio) holds it, no
partner is linked to it, and it has no label formula, which could read
what changes
*/
bool projectDataBase::canEditUnbuilt(const QUuid &element) const
{
if (!hasElementInformation(element) || placedElementCount(element) != 0
|| !linkedSymbols(element).isEmpty()
|| !m_element_information.value(element).value(QStringLiteral("formula")).toString().isEmpty()) {
return false;
}
QSqlQuery query(m_data_base);
query.prepare(QStringLiteral("SELECT COUNT(*) FROM element WHERE uuid = :uuid"));
query.bindValue(QStringLiteral(":uuid"), element.toString());
return query.exec() && query.next() && query.value(0).toInt() == 1;
}
/**
@brief projectDataBase::editUnbuiltElementInformation
Change the information of the symbol @p element, on a folio not built
(canEditUnbuilt()), to @p information, as Element::setElementInformations()
would: the store and the symbol's element info row. Building the symbol
takes it (takeUnbuiltEdit()); saving builds its folio first
(foliosWithUnbuiltEdits()).
*/
void projectDataBase::editUnbuiltElementInformation(const QUuid &element, DiagramContext information)
{
if (information.contains(QStringLiteral("formula_id"))
&& information.value(QStringLiteral("formula")).toString().isEmpty()) {
information.remove(QStringLiteral("formula_id"));
}
QUuid folio;
{
QSqlQuery query(m_data_base);
query.prepare(QStringLiteral("SELECT diagram_uuid FROM element WHERE uuid = :uuid"));
query.bindValue(QStringLiteral(":uuid"), element.toString());
if (query.exec() && query.next()) folio = QUuid(query.value(0).toString());
}
//Whether the file has a block of information for it: a symbol
//saves one when it has any (Element::toXml())
const bool had_information = m_unbuilt_edits.contains(element)
? m_unbuilt_edits.value(element).had_information
: !m_element_information.value(element).keys().isEmpty();
m_unbuilt_edits.insert(element, {information, folio, had_information});
if (!storeElementInformation(element, information)) {
return;
}
m_content_changed = true;
if (!m_update_blocked) {
bindElementInfoValues(m_update_element_query, element.toString(), information,
information.value(QStringLiteral("label")).toString());
if (!m_update_element_query.exec()) {
qDebug() << "projectDataBase::editUnbuiltElementInformation update error : " << m_update_element_query.lastError();
}
}
emit dataBaseUpdated();
}
/**
@brief projectDataBase::takeUnbuiltEdit
@return the information @p element was given while its folio was not
built (editUnbuiltElementInformation()), forgotten here: the symbol
being built takes it. Nothing if it was given none.
*/
std::optional<DiagramContext> projectDataBase::takeUnbuiltEdit(const QUuid &element)
{
const auto edit = m_unbuilt_edits.constFind(element);
if (edit == m_unbuilt_edits.constEnd()) {
return std::nullopt;
}
const DiagramContext information = edit->information;
m_unbuilt_edits.erase(edit);
return information;
}
/**
@brief projectDataBase::foliosWithUnbuiltEdits
@return the folios holding a symbol whose information was changed while
they were not built (editUnbuiltElementInformation())
*/
QSet<QUuid> projectDataBase::foliosWithUnbuiltEdits() const
{
QSet<QUuid> folios;
for (const auto &edit : m_unbuilt_edits) folios.insert(edit.folio);
return folios;
}
/**
@brief projectDataBase::unbuiltEditsOnFolio
@return the information changed through the store of the symbols on the
folio @p folio, not built (editUnbuiltElementInformation()), by symbol.
@p need_build is set when a .qetz cannot save one in place of the rows it
was read with: the file has no block of information for the symbol, or
the change empties it (a symbol with none saves no block)
*/
QHash<QUuid, DiagramContext> projectDataBase::unbuiltEditsOnFolio(const QUuid &folio, bool *need_build) const
{
QHash<QUuid, DiagramContext> edits;
if (need_build) *need_build = false;
for (auto it = m_unbuilt_edits.cbegin() ; it != m_unbuilt_edits.cend() ; ++it) {
if (it->folio != folio) continue;
edits.insert(it.key(), it->information);
if (need_build && (!it->had_information || it->information.keys().isEmpty()))
*need_build = true;
}
return edits;
}
/**
@brief projectDataBase::setPlacedElementInformation
Give the symbol @p element its information @p information: through the
symbol when it is built, through the store when it is not
(canEditUnbuilt()). What undoing and redoing a change of a symbol not
built does, the symbol having perhaps been built since.
@return false if neither can
*/
bool projectDataBase::setPlacedElementInformation(const QUuid &element, const DiagramContext &information)
{
const QList<QPointer<Element>> placed = m_placed_elements.values(element);
if (placed.size() == 1 && placed.first()) {
placed.first()->setElementInformations(information);
elementInfoChanged(placed.first().data());
return true;
}
if (placed.isEmpty() && m_unbuilt_edits.contains(element)) {
editUnbuiltElementInformation(element, information);
return true;
}
if (placed.isEmpty() && canEditUnbuilt(element)) {
editUnbuiltElementInformation(element, information);
return true;
}
return false;
}
/**
@brief projectDataBase::folioUuidsOfElements
@return the uuids of the folios the symbols @p elements are on, read
+11
View File
@@ -180,6 +180,12 @@ class projectDataBase : public QObject
QString prefillReport() const;
QSet<QUuid> foliosWithUnjoinedWires() const;
QSet<QUuid> folioUuidsOfElements(const QSet<QUuid> &elements) const;
bool canEditUnbuilt(const QUuid &element) const;
void editUnbuiltElementInformation(const QUuid &element, DiagramContext information);
std::optional<DiagramContext> takeUnbuiltEdit(const QUuid &element);
QSet<QUuid> foliosWithUnbuiltEdits() const;
QHash<QUuid, DiagramContext> unbuiltEditsOnFolio(const QUuid &folio, bool *need_build = nullptr) const;
bool setPlacedElementInformation(const QUuid &element, const DiagramContext &information);
QHash<QUuid, QString> elementTypesOnFolios(const QSet<QUuid> &folios) const;
/// A symbol placed on a folio, as the element table has it
struct FolioSymbol {
@@ -335,6 +341,11 @@ class projectDataBase : public QObject
//briefly gives a copy its original's uuid, so one uuid can have
//two symbols until the copy's is renewed.
QMultiHash<QUuid, QPointer<Element>> m_placed_elements;
/// Information changed through the store for symbols on folios
/// not built (editUnbuiltElementInformation()), with the folio
/// each is on: building the symbol takes it (takeUnbuiltEdit())
struct UnbuiltEdit { DiagramContext information; QUuid folio; bool had_information; };
QHash<QUuid, UnbuiltEdit> m_unbuilt_edits;
QHash<QUuid, TitleBlockProperties> m_folio_titleblocks;
QHash<QUuid, QPointer<Diagram>> m_placed_folios;
QHash<QUuid, ConductorProperties> m_conductor_properties;
+5
View File
@@ -1115,6 +1115,11 @@ bool Element::fromXml(QDomElement &e,
DiagramContext dc;
dc.fromXml(e.firstChildElement(QStringLiteral("elementInformations")),
QStringLiteral("elementInformation"));
//Changed through the project database while its folio was not
//built (QET_LAZY_FOLIOS): the change, not what the file says
if (diagram() && diagram()->project())
if (const auto edited = diagram()->project()->dataBase()->takeUnbuiltEdit(m_uuid))
dc = *edited;
//Load override properties (For now, only used when the element is a terminal)
if (m_data.m_type == ElementData::Terminal)
+25 -3
View File
@@ -2360,9 +2360,20 @@ QDomDocument QETProject::toXml(QHash<int, QByteArray> *kept_folios, QStringList
if (keep) {
//Partners built alone and linked or unlinked since: their folio
//is not what was read
for (Diagram *diagram : std::as_const(m_diagrams_list))
if (!diagram->isLoaded() && diagram->partialChanged())
buildFolios({diagram});
//and symbols whose information was changed through the project
//database while their folio was not built: built together
//-- a .qetz writes those in its database instead, when it can
const QSet<QUuid> edited = m_data_base.foliosWithUnbuiltEdits();
QList<Diagram *> changed;
for (Diagram *diagram : std::as_const(m_diagrams_list)) {
if (diagram->isLoaded()) continue;
bool need_build = !kept_parts;
if (edited.contains(diagram->uuid()) && kept_parts)
m_data_base.unbuiltEditsOnFolio(diagram->uuid(), &need_build);
if (diagram->partialChanged() || (edited.contains(diagram->uuid()) && need_build))
changed << diagram;
}
buildFolios(changed);
for (Diagram *diagram : std::as_const(m_diagrams_list))
if (diagram->headerChanged())
buildFolioToShow(diagram);
@@ -2483,6 +2494,17 @@ QDomDocument QETProject::toXml(QHash<int, QByteArray> *kept_folios, QStringList
auto placeholder = xml_doc.createElement(QStringLiteral("diagram"));
placeholder.setAttribute(QStringLiteral("qetz-kept-part"), diagram->deferredPart());
placeholder.setAttribute(QStringLiteral("uuid"), diagram->uuid().toString());
//Symbols whose information changed while the folio was
//not built: the rows QetContainer::split() writes for them
const QHash<QUuid, DiagramContext> edits = m_data_base.unbuiltEditsOnFolio(diagram->uuid());
for (auto it = edits.cbegin() ; it != edits.cend() ; ++it) {
auto symbol = xml_doc.createElement(QStringLiteral("qetz-edited-element"));
symbol.setAttribute(QStringLiteral("uuid"), it.key().toString());
auto information = xml_doc.createElement(QStringLiteral("elementInformations"));
it.value().toXml(information, QStringLiteral("elementInformation"));
symbol.appendChild(information);
placeholder.appendChild(symbol);
}
kept_parts->append(diagram->deferredPart());
project_root.appendChild(placeholder);
++order_num;
+37
View File
@@ -462,6 +462,28 @@ void QetScriptApi::setLive(QStringList *log)
m_live_log = log;
}
/**
@brief QetScriptApi::unbuiltInformation
@return the information of the symbol @p elementUuid on the folio
@p folioIndex when that folio is not built (QET_LAZY_FOLIOS) and the
symbol's information can be read and changed through the project
database alone (projectDataBase::canEditUnbuilt()); nothing otherwise,
the folio to be built
*/
std::optional<DiagramContext> QetScriptApi::unbuiltInformation(int folioIndex, const QString &elementUuid) const
{
if (!m_project || folioIndex < 0 || folioIndex >= m_project->folios().count()) return std::nullopt;
Diagram *folio = m_project->folios().at(folioIndex);
const QUuid uuid(elementUuid);
projectDataBase *database = m_project->dataBase();
if (folio->isLoaded() || !database->rowsFromDocument()
|| !database->folioUuidsOfElements({uuid}).contains(folio->uuid())
|| !database->canEditUnbuilt(uuid)) {
return std::nullopt;
}
return database->elementInformation(uuid);
}
/**
@brief QetScriptApi::built
@return @p folio, built if it was not yet (QET_LAZY_FOLIOS), as showing
@@ -666,6 +688,19 @@ bool QetScriptApi::setInfoKey(int folioIndex, const QString &elementUuid,
log(QStringLiteral("qet.%1: \"%2\" is internal, follow a numbering with numberElement()")
.arg(caller, key));
return false;
}
//A symbol on a folio not built whose information nothing else
//shows is changed through the project database, its folio left
//unbuilt (stage D5)
if (key != QETInformation::ELMT_FORMULA) {
if (const auto stored = unbuiltInformation(folioIndex, elementUuid)) {
if (stored->value(key).toString() == value) return true; // nothing to push
DiagramContext new_info = *stored;
new_info.addValue(key, value);
m_project->undoStack()->push(new ChangeStoredElementInformationCommand(
m_project, QUuid(elementUuid), *stored, new_info));
return true;
}
}
Element *element = findElement(folioIndex, elementUuid);
if (!element) return false;
@@ -1009,6 +1044,8 @@ int QetScriptApi::terminalIndex(int folioIndex, const QString &elementUuid,
QString QetScriptApi::elementInfo(int folioIndex, const QString &elementUuid, const QString &key) const
{
if (const auto stored = unbuiltInformation(folioIndex, elementUuid))
return stored->value(key).toString();
Element *element = findElement(folioIndex, elementUuid);
if (!element) return QString();
return element->elementInformations().value(key).toString();
+5
View File
@@ -24,6 +24,10 @@
#include <QVariantList>
#include <QVariantMap>
#include <optional>
#include "../diagramcontext.h"
class QETProject;
class Diagram;
class DiagramView;
@@ -644,6 +648,7 @@ class QetScriptApi : public QObject
private:
bool runFlag(const QString &flag, const QStringList &args);
Diagram *built(Diagram *folio) const;
std::optional<DiagramContext> unbuiltInformation(int folioIndex, const QString &elementUuid) const;
Element *findElement(int folioIndex, const QString &elementUuid) const;
Terminal *findTerminal(int folioIndex, const QString &elementUuid, int terminalIndex,
const QString &caller);
@@ -119,3 +119,27 @@ void ChangeElementInformationCommand::updateProjectDB()
elmt->diagram()->project()->dataBase()->elementInfoChanged(list_);
}
}
ChangeStoredElementInformationCommand::ChangeStoredElementInformationCommand(
QETProject *project, const QUuid &element, const DiagramContext &old_info,
const DiagramContext &new_info, QUndoCommand *parent) :
QUndoCommand(parent),
m_project(project),
m_element(element),
m_old_info(old_info),
m_new_info(new_info)
{
setText(QObject::tr("Change the information of the element"));
}
void ChangeStoredElementInformationCommand::undo()
{
if (m_project)
m_project->dataBase()->setPlacedElementInformation(m_element, m_old_info);
}
void ChangeStoredElementInformationCommand::redo()
{
if (m_project)
m_project->dataBase()->setPlacedElementInformation(m_element, m_new_info);
}
@@ -21,8 +21,11 @@
#include "../diagramcontext.h"
#include <QUndoCommand>
#include <QUuid>
#include <QPointer>
class Element;
class QETProject;
/**
@brief The ChangeElementInformationCommand class
@@ -52,4 +55,26 @@ class ChangeElementInformationCommand : public QUndoCommand
QMap<QPointer<Element>, QPair<DiagramContext, DiagramContext>> m_map;
};
/**
@brief The ChangeStoredElementInformationCommand class
Change the information of a symbol given by its uuid, built or not
(QET_LAZY_FOLIOS): through the symbol when it is built, through the
project database when it is not (projectDataBase::canEditUnbuilt()).
*/
class ChangeStoredElementInformationCommand : public QUndoCommand
{
public:
ChangeStoredElementInformationCommand(QETProject *project, const QUuid &element,
const DiagramContext &old_info,
const DiagramContext &new_info,
QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
QPointer<QETProject> m_project;
QUuid m_element;
DiagramContext m_old_info, m_new_info;
};
#endif // CHANGEELEMENTINFORMATIONCOMMAND_H
+51
View File
@@ -829,6 +829,57 @@ int main(int argc, char **argv)
check(unbuilt - scripted.unloadedFolioCount() == 1, "a script reading one folio builds that folio only");
}
//A script changes the information of a symbol on a folio not
//built, which nothing else shows: through the project
//database, no folio built; saved as when built; undone (D5)
{
QETProject changed(zipped);
const int unbuilt = changed.unloadedFolioCount();
int folio_index = -1;
QString uuid;
for (int i = 0 ; i < folios.size() && uuid.isEmpty() ; ++i) {
if (changed.folios().at(i)->isLoaded()) continue;
for (Element *element : folios.at(i)->elements())
if (changed.dataBase()->canEditUnbuilt(element->uuid())) {
folio_index = i; uuid = element->uuid().toString(); break;
}
}
check(!uuid.isEmpty(), "a symbol to change through the database");
QetScriptApi api(&changed);
const QString before = api.elementInfo(folio_index, uuid, "comment");
const QString value = QStringLiteral("changed through the database");
check(api.setElementInfo(folio_index, uuid, "comment", value), "a script changes a symbol not built");
check(api.elementInfo(folio_index, uuid, "comment") == value, "the change read back");
check(changed.unloadedFolioCount() == unbuilt, "changing a symbol not built builds no folio");
const QString changed_path = output + "/changed.qetz";
check(changed.writeContainerFile(changed_path, &error), "saved after the change");
std::printf(".qetz symbol on folio %d changed by a script: %d folios built, %d by saving\n",
folio_index + 1, 0, unbuilt - changed.unloadedFolioCount());
qunsetenv("QET_LAZY_FOLIOS");
const QString usual_path = output + "/changed-usual.qetz";
{
QETProject usual_changed(zipped);
QetScriptApi usual_api(&usual_changed);
check(usual_api.setElementInfo(folio_index, uuid, "comment", value), "the same change when built");
check(usual_changed.writeContainerFile(usual_path, &error), "saved after the change when built");
}
QETProject changed_back(changed_path), usual_back(usual_path);
qputenv("QET_LAZY_FOLIOS", "1");
check(sortedLines(changed_back.toXml().toString(), path) == sortedLines(usual_back.toXml().toString(), path),
"a symbol changed through the database saved as when built");
check(QetScriptApi(&changed_back).elementInfo(folio_index, uuid, "comment") == value, "the change saved");
//Undone, then redone and the folio built: the symbol shows it
changed.undoStack()->undo();
check(api.elementInfo(folio_index, uuid, "comment") == before, "the change undone");
changed.undoStack()->redo();
changed.buildFolioToShow(changed.folios().at(folio_index));
Element *built_symbol = nullptr;
for (Element *element : changed.folios().at(folio_index)->elements())
if (element->uuid().toString() == uuid) built_symbol = element;
check(built_symbol && built_symbol->elementInformations().value("comment").toString() == value,
"a symbol changed before its folio was built shows the change");
}
//The wire numbers and wiring list exports, and the terminal
//numbering dialog: as when built, building only the folios
//holding a terminal (D5)