diff --git a/sources/container/qetcontainerdb.cpp b/sources/container/qetcontainerdb.cpp index 541b1d00f..f455f945a 100644 --- a/sources/container/qetcontainerdb.cpp +++ b/sources/container/qetcontainerdb.cpp @@ -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)); diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index 74e391b5f..b076e92a0 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -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 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 projectDataBase::foliosWithUnbuiltEdits() const +{ + QSet 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 projectDataBase::unbuiltEditsOnFolio(const QUuid &folio, bool *need_build) const +{ + QHash 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> 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 diff --git a/sources/dataBase/projectdatabase.h b/sources/dataBase/projectdatabase.h index 4170a288c..3448346ac 100644 --- a/sources/dataBase/projectdatabase.h +++ b/sources/dataBase/projectdatabase.h @@ -180,6 +180,12 @@ class projectDataBase : public QObject QString prefillReport() const; QSet foliosWithUnjoinedWires() const; QSet folioUuidsOfElements(const QSet &elements) const; + bool canEditUnbuilt(const QUuid &element) const; + void editUnbuiltElementInformation(const QUuid &element, DiagramContext information); + std::optional takeUnbuiltEdit(const QUuid &element); + QSet foliosWithUnbuiltEdits() const; + QHash unbuiltEditsOnFolio(const QUuid &folio, bool *need_build = nullptr) const; + bool setPlacedElementInformation(const QUuid &element, const DiagramContext &information); QHash elementTypesOnFolios(const QSet &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> 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 m_unbuilt_edits; QHash m_folio_titleblocks; QHash> m_placed_folios; QHash m_conductor_properties; diff --git a/sources/qetgraphicsitem/element.cpp b/sources/qetgraphicsitem/element.cpp index ae497bafb..87fc394b5 100644 --- a/sources/qetgraphicsitem/element.cpp +++ b/sources/qetgraphicsitem/element.cpp @@ -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) diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index b788313c9..63d6a1239 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -2360,9 +2360,20 @@ QDomDocument QETProject::toXml(QHash *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 edited = m_data_base.foliosWithUnbuiltEdits(); + QList 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 *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 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; diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index 953e355b8..32247aed9 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -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 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(); diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index 87f30aca5..6a954d5a6 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -24,6 +24,10 @@ #include #include +#include + +#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 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); diff --git a/sources/undocommand/changeelementinformationcommand.cpp b/sources/undocommand/changeelementinformationcommand.cpp index bf7045b9d..7a76220e5 100644 --- a/sources/undocommand/changeelementinformationcommand.cpp +++ b/sources/undocommand/changeelementinformationcommand.cpp @@ -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); +} diff --git a/sources/undocommand/changeelementinformationcommand.h b/sources/undocommand/changeelementinformationcommand.h index 6e103ebf6..1d7d2cf70 100644 --- a/sources/undocommand/changeelementinformationcommand.h +++ b/sources/undocommand/changeelementinformationcommand.h @@ -21,8 +21,11 @@ #include "../diagramcontext.h" #include +#include +#include class Element; +class QETProject; /** @brief The ChangeElementInformationCommand class @@ -52,4 +55,26 @@ class ChangeElementInformationCommand : public QUndoCommand QMap, QPair> 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 m_project; + QUuid m_element; + DiagramContext m_old_info, m_new_info; +}; + #endif // CHANGEELEMENTINFORMATIONCOMMAND_H diff --git a/tests/qttest/lazy_folios_probe.cpp b/tests/qttest/lazy_folios_probe.cpp index 39b69751b..0375b24f5 100644 --- a/tests/qttest/lazy_folios_probe.cpp +++ b/tests/qttest/lazy_folios_probe.cpp @@ -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)