diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index 2037eb121..e8c7b92b2 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -17,7 +17,6 @@ */ #include "projectdatabase.h" #include "../qetversion.h" - #include "sqlreadonly.h" #include "../autoNum/assignvariables.h" @@ -57,6 +56,7 @@ #include #include #include +#include @@ -868,6 +868,7 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString } } } + m_holds_links = true; for (const DocumentConductor &conductor : std::as_const(conductors)) { @@ -1654,8 +1655,13 @@ void projectDataBase::addElement(Element *element) */ void projectDataBase::removeElement(Element *element) { - m_content_changed = true; const QUuid uuid = element->uuid(); + //A symbol built alone as a partner (Diagram::buildSymbols()) was + //never placed: its rows are those of the symbol its folio builds + if (!m_placed_elements.contains(uuid, element)) { + return; + } + m_content_changed = true; removeElementRows(uuid); unplaceElement(element, uuid); //Another symbol still holds the uuid (F100): the rows are its own. @@ -3979,8 +3985,16 @@ QString projectDataBase::prefillReport() const void projectDataBase::populateLinkTable() { m_dirty_link_elements.clear(); - const QSet waiting = waitingSymbols(); + const QSet waiting_symbols = waitingSymbols(); + const auto waiting = [&waiting_symbols](const QUuid &uuid) {return waiting_symbols.contains(uuid);}; QSqlQuery query(m_data_base); + //The rows as they are: the order they give, and the partners on + //folios not built, which no built symbol knows + QHash>> old_rows; + if (query.exec(QStringLiteral("SELECT element_uuid, linked_uuid, group_index FROM link ORDER BY ord, rowid"))) + while (query.next()) + old_rows[QUuid(query.value(0).toString())].append( + {QUuid(query.value(1).toString()), query.value(2).isNull() ? -1 : query.value(2).toInt()}); query.exec(QStringLiteral("DELETE FROM link")); query.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index, ord) " "VALUES (:element_uuid, :linked_uuid, :group_index, :ord)")); @@ -3997,8 +4011,11 @@ void projectDataBase::populateLinkTable() } const ElementProvider ep(diagram); for (const auto &elmt : ep.find(allElementTypes())) - insertLinkRows(query, elmt, waiting); + insertLinkRows(query, elmt, waiting, + old_rows.contains(elmt->uuid()) ? old_rows.value(elmt->uuid()) + : elmt->savedLinkList()); } + m_holds_links = true; } /** @@ -4026,8 +4043,9 @@ void projectDataBase::queueLinks(Element *element) /** @brief projectDataBase::linkRowMismatches A test and debugging aid (QET_CHECK_LINK_ROWS): the placed - symbols whose link rows are not the links they save, in order - (Element::linksToSave(), insertLinkRows()) + symbols whose link rows are not their links: each built partner, in + the order the symbol has them, and no other, but partners on folios + not built (QETZ-DB-FIRST stage 2a-3: the rows are what is saved) @return one entry per such symbol: its uuid */ QStringList projectDataBase::linkRowMismatches() @@ -4035,26 +4053,29 @@ QStringList projectDataBase::linkRowMismatches() flushLinks(); const QSet waiting = waitingSymbols(); QStringList mismatches; - QSqlQuery query(m_data_base); - query.prepare(QStringLiteral("SELECT linked_uuid, group_index FROM link " - "WHERE element_uuid = :uuid ORDER BY ord, rowid")); for (auto it = m_placed_elements.cbegin() ; it != m_placed_elements.cend() ; ++it) { Element *element = it.value(); if (!element) { continue; } - QList> expected; - for (const Element::SavedLink &link : element->linksToSave(waiting)) - if (!link.built || m_placed_elements.contains(link.uuid, link.built)) - expected.append({link.uuid.toString(), link.group_index}); - QList> rows; - query.bindValue(QStringLiteral(":uuid"), element->uuid().toString()); - if (query.exec()) - while (query.next()) - rows.append({query.value(0).toString(), - query.value(1).isNull() ? -1 : query.value(1).toInt()}); - if (rows != expected) { + QList> expected; + for (Element *linked : element->linkedElements()) + if (m_placed_elements.contains(linked->uuid(), linked) || builtAlone(linked)) + expected.append({linked->uuid(), element->groupIndexForElement(linked)}); + QList> rows; + bool stray = false; + for (const auto &row : linkRowsOf(element->uuid())) { + bool built = false; + for (const auto &link : std::as_const(expected)) + built |= link.first == row.first; + if (built) { + rows.append(row); + } else { + stray |= !waiting.contains(row.first); + } + } + if (stray || rows != expected) { mismatches << element->uuid().toString(); } } @@ -4096,36 +4117,133 @@ bool projectDataBase::savedLinks(const Element *element, QList projectDataBase::waitingSymbols() const { - return m_project && m_project->unloadedFolioCount() ? m_project->symbolsOnUnbuiltFolios() - : QSet(); + if (!m_project || !m_project->unloadedFolioCount()) { + return QSet(); + } + //Kept while the same folios are not built and as many symbols are + //built alone: finding them reads every such folio's rows + QList unbuilt; + int alone = 0; + for (Diagram *diagram : m_project->folios()) + if (!diagram->isLoaded()) { + unbuilt << diagram; + alone += int(diagram->partialElements().size()); + } + if (unbuilt == m_waiting_folios && alone == m_waiting_alone) { + return m_waiting_symbols; + } + //Folios only built since: their symbols are not waiting any more + bool only_built = true; + for (Diagram *diagram : std::as_const(unbuilt)) + only_built &= m_waiting_folios.contains(diagram); + for (Diagram *diagram : std::as_const(m_waiting_folios)) + if (!unbuilt.contains(diagram)) + only_built &= m_project->folios().contains(diagram) && diagram->isLoaded(); + QSet waiting; + if (only_built && !m_waiting_folios.isEmpty()) { + waiting = m_waiting_symbols; + for (Diagram *diagram : std::as_const(m_waiting_folios)) + if (!unbuilt.contains(diagram)) + for (Element *element : diagram->elements()) + waiting.remove(element->uuid()); + } else { + waiting = m_project->symbolsOnUnbuiltFolios(); + } + for (Diagram *diagram : std::as_const(unbuilt)) + for (const QUuid &built : diagram->partialElements().keys()) + waiting.remove(built); + m_waiting_folios = unbuilt; + m_waiting_alone = alone; + m_waiting_symbols = waiting; + return waiting; +} + +/** + @brief projectDataBase::builtAlone + @return true if @p element is a symbol built alone, as a partner, on a + folio of the project not built (Diagram::buildSymbols()) +*/ +bool projectDataBase::builtAlone(Element *element) const +{ + Diagram *diagram = element->diagram(); + return m_project && diagram && !diagram->isLoaded() + && m_project->folios().contains(diagram) + && diagram->partialElements().value(element->uuid()) == element; +} + +/** + @brief projectDataBase::linksLater + @return true if a symbol of uuid @p uuid is placed but not linked from + its rows yet (Element::initLink(), as its folio is being built): it + links to the partners its rows and theirs name, so these are kept +*/ +bool projectDataBase::linksLater(const QUuid &uuid) const +{ + for (const QPointer &element : m_placed_elements.values(uuid)) + if (element && !element->savedLinkList().isEmpty()) + return true; + return false; +} + +/** + @brief projectDataBase::rowsToWrite + @return the link rows of @p element: its built partners, and the + partners on folios not built (@p waiting) that @p old_rows, its rows as + they were, name, and those placed but not linked yet (linksLater()). + The rows keep the order of @p old_rows, the order the file gave, and + partners linked since come after them: the order the symbol has its + partners in changes with the order symbols are built. + A built partner the project no longer has (on a removed folio, linked + still for the undo) gets no row. +*/ +QList> projectDataBase::rowsToWrite(Element *element, const std::function &waiting, + const QList> &old_rows) const +{ + QList built; + for (Element *linked : element->linkedElements()) + if (m_placed_elements.contains(linked->uuid(), linked) || builtAlone(linked)) + built << linked; + QList> rows; + QSet written; + for (const auto &row : old_rows) { + Element *linked = nullptr; + for (Element *partner : std::as_const(built)) + if (partner->uuid() == row.first && !written.contains(partner)) { linked = partner; break; } + if (linked) { + written.insert(linked); + rows.append({linked->uuid(), element->groupIndexForElement(linked)}); + } else if (waiting(row.first) || linksLater(row.first)) { + rows.append(row); + } + } + for (Element *partner : std::as_const(built)) + if (!written.contains(partner)) + rows.append({partner->uuid(), element->groupIndexForElement(partner)}); + return rows; } /** @brief projectDataBase::insertLinkRows Write @p element's link rows with @p insert (prepared with :element_uuid, - :linked_uuid, :group_index and :ord): its partners as it saves them - (Element::linksToSave()), in that order, so the rows and the file say - the same: @p waiting is the symbols on folios not built, as a save - takes them (waitingSymbols()). A built partner the project no longer has (on a removed - folio, linked still for the undo) gets no row. + :linked_uuid, :group_index and :ord), as rowsToWrite() gives them, in + that order: @p waiting is the symbols on folios not built + (waitingSymbols()) tells which, @p old_rows its rows before. */ -void projectDataBase::insertLinkRows(QSqlQuery &insert, Element *element, - const QSet &waiting) +void projectDataBase::insertLinkRows(QSqlQuery &insert, Element *element, const std::function &waiting, + const QList> &old_rows) { int ord = 0; - for (const Element::SavedLink &link : element->linksToSave(waiting)) + for (const auto &link : rowsToWrite(element, waiting, old_rows)) { - if (link.built && !m_placed_elements.contains(link.uuid, link.built)) { - continue; - } insert.bindValue(QStringLiteral(":element_uuid"), element->uuid().toString()); - insert.bindValue(QStringLiteral(":linked_uuid"), link.uuid.toString()); - insert.bindValue(QStringLiteral(":group_index"), link.group_index >= 0 ? QVariant(link.group_index) : QVariant()); + insert.bindValue(QStringLiteral(":linked_uuid"), link.first.toString()); + insert.bindValue(QStringLiteral(":group_index"), link.second >= 0 ? QVariant(link.second) : QVariant()); insert.bindValue(QStringLiteral(":ord"), ord++); if (!insert.exec()) { qDebug() << "projectDataBase::insertLinkRows insert error : " << insert.lastError(); @@ -4133,6 +4251,65 @@ void projectDataBase::insertLinkRows(QSqlQuery &insert, Element *element, } } +/** + @brief projectDataBase::linkRowsOf + @return the link rows of @p element: each partner and its contact group + (-1 if none), in their order +*/ +QList> projectDataBase::linkRowsOf(const QUuid &element) const +{ + QList> rows; + QSqlQuery query(m_data_base); + query.prepare(QStringLiteral("SELECT linked_uuid, group_index FROM link " + "WHERE element_uuid = :uuid ORDER BY ord, rowid")); + query.bindValue(QStringLiteral(":uuid"), element.toString()); + if (query.exec()) + while (query.next()) + rows.append({QUuid(query.value(0).toString()), + query.value(1).isNull() ? -1 : query.value(1).toInt()}); + return rows; +} + +/** + @brief projectDataBase::partnersToLink + @return the partners a symbol built links to (Element::initLink()): its + link rows, in their order, then the symbols whose rows name it (no + contact group: the group is the row owner's) +*/ +QList> projectDataBase::partnersToLink(const QUuid &element) +{ + flushLinks(); + QList> partners = linkRowsOf(element); + QSqlQuery query(m_data_base); + query.prepare(QStringLiteral("SELECT element_uuid FROM link WHERE linked_uuid = :uuid ORDER BY rowid")); + query.bindValue(QStringLiteral(":uuid"), element.toString()); + if (query.exec()) + while (query.next()) + partners.append({QUuid(query.value(0).toString()), -1}); + return partners; +} + +/** + @brief projectDataBase::waitingPartners + @return the partners of @p element its link rows name among @p waiting, + the symbols on folios not built (waitingSymbols()): those it is not + linked to yet +*/ +QSet projectDataBase::waitingPartners(const QUuid &element, const QSet &waiting) +{ + QSet partners; + if (waiting.isEmpty()) { + return partners; + } + flushLinks(); + //A partner built alone (Diagram::buildSymbols()) is built: its + //folio, not built, still holds it + for (const auto &row : linkRowsOf(element)) + if (waiting.contains(row.first) && waitingSymbols().contains(row.first)) + partners.insert(row.first); + return partners; +} + /** @brief projectDataBase::flushLinks Write the link rows of every element queued by linksChanged() @@ -4145,18 +4322,42 @@ void projectDataBase::flushLinks() const auto dirty = m_dirty_link_elements; m_dirty_link_elements.clear(); - const QSet waiting = waitingSymbols(); + //The symbols on folios not built, looked up only when a row names + //a partner not linked: finding them reads every such folio's rows + std::optional> waiting_symbols; + const auto waiting = [this, &waiting_symbols](const QUuid &uuid) { + if (!waiting_symbols) { + waiting_symbols = waitingSymbols(); + } + return waiting_symbols->contains(uuid); + }; const bool own_transaction = m_data_base.transaction(); QSqlQuery remove(m_data_base); remove.prepare(QStringLiteral("DELETE FROM link WHERE element_uuid = :uuid")); QSqlQuery insert(m_data_base); insert.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index, ord) " "VALUES (:element_uuid, :linked_uuid, :group_index, :ord)")); + QList> not_linked; for (const QPointer &element : dirty) { if (!element) { continue; } + const bool placed = m_placed_elements.contains(element->uuid(), element); + //A symbol not linked from its rows yet (Element::initLink()) + //keeps them as they are: a partner linking to it first has not + //given it its contact groups. Written once it is linked. + if (placed && m_holds_links && !element->savedLinkList().isEmpty()) { + not_linked << element; + continue; + } + QList> old_rows; + if (placed) { + old_rows = linkRowsOf(element->uuid()); + if (old_rows.isEmpty()) { + old_rows = element->savedLinkList(); + } + } remove.bindValue(QStringLiteral(":uuid"), element->uuid().toString()); if (!remove.exec()) { qDebug() << "projectDataBase::flushLinks remove error : " << remove.lastError(); @@ -4164,14 +4365,15 @@ void projectDataBase::flushLinks() //An element taken off its folio is unlinked first (Diagram:: //removeItem()), so it writes no rows here; one on a removed //folio is not, and writes none either. - if (!m_placed_elements.contains(element->uuid(), element)) { + if (!placed) { continue; } - insertLinkRows(insert, element, waiting); + insertLinkRows(insert, element, waiting, old_rows); } if (own_transaction) { m_data_base.commit(); } + m_dirty_link_elements << not_linked; } void projectDataBase::populateDiagramInfoTable() diff --git a/sources/dataBase/projectdatabase.h b/sources/dataBase/projectdatabase.h index e8001680e..da8d4103b 100644 --- a/sources/dataBase/projectdatabase.h +++ b/sources/dataBase/projectdatabase.h @@ -18,6 +18,7 @@ #ifndef PROJECTDATABASE_H #define PROJECTDATABASE_H +#include #include #include #include @@ -156,6 +157,14 @@ class projectDataBase : public QObject QStringList conductorPropertiesMismatches() const; QStringList linkRowMismatches(); bool savedLinks(const Element *element, QList> *links); + //The link rows are written (opening filled them): a symbol + //built links from them (Element::initLink()) + bool holdsLinks() const {return m_holds_links;} + void forgetLinks() {m_holds_links = false;} + QList> partnersToLink(const QUuid &element); + QList> linkRowsOf(const QUuid &element) const; + QSet waitingPartners(const QUuid &element, const QSet &waiting); + QSet waitingSymbols() const; //Fill the three stores from a project's document before its //folios are built, then, once @@ -257,8 +266,12 @@ class projectDataBase : public QObject void populateConductorTable(); void populateLinkTable(); void flushLinks(); - void insertLinkRows(QSqlQuery &insert, Element *element, const QSet &waiting); - QSet waitingSymbols() const; + void insertLinkRows(QSqlQuery &insert, Element *element, const std::function &waiting, + const QList> &old_rows); + bool builtAlone(Element *element) const; + bool linksLater(const QUuid &uuid) const; + QList> rowsToWrite(Element *element, const std::function &waiting, + const QList> &old_rows) const; void populateDrawingItemTables(); bool populateFromDocument(const QDomDocument &document, QString *why = nullptr); bool writeDrawingItem(QObject *object); @@ -334,6 +347,12 @@ class projectDataBase : public QObject //so the rows are written when next read, as drawing items are. QList> m_dirty_link_elements; QSqlQuery m_saved_links_query; + bool m_holds_links = false; + /// waitingSymbols() as it was last, for these folios not built + /// and this count of symbols built alone on them + mutable QList m_waiting_folios; + mutable int m_waiting_alone = 0; + mutable QSet m_waiting_symbols; //Elements moved since their folio cell was last written: a move //sends xChanged and yChanged for every step of a drag, so the //cell is written when next read, as drawing items are. diff --git a/sources/elementprovider.cpp b/sources/elementprovider.cpp index 3bb4c4165..19de3126c 100644 --- a/sources/elementprovider.cpp +++ b/sources/elementprovider.cpp @@ -22,6 +22,7 @@ #include "qetgraphicsitem/element.h" #include "qetgraphicsitem/terminalelement.h" #include "qetproject.h" +#include "dataBase/projectdatabase.h" #include @@ -111,7 +112,8 @@ QVector > ElementProvider::freeElement(ElementData::Types filt { if (filter & elmt_->elementData().m_type && elmt_->isFree() && - !elmt_->pendingLinks().intersects(waiting)) + (waiting.isEmpty() + || m_project->dataBase()->waitingPartners(elmt_->uuid(), waiting).isEmpty())) { free_elmt << elmt_; } diff --git a/sources/qetgraphicsitem/element.cpp b/sources/qetgraphicsitem/element.cpp index 5227ca7c4..e9eacba5f 100644 --- a/sources/qetgraphicsitem/element.cpp +++ b/sources/qetgraphicsitem/element.cpp @@ -1440,16 +1440,14 @@ QDomElement Element::toXml( //if this element is linked to other elements, save the uuid of each: //as the project database's link rows list them (QETZ-DB-FIRST stage - //2a), or, for a symbol the database does not hold, as it lists them - //itself (linksToSave()) + //2a), or, for a symbol the database does not hold, its partners QList> links; projectDataBase *database = diagram() && diagram()->project() ? diagram()->project()->dataBase() : nullptr; if (!database || !database->savedLinks(this, &links)) { links.clear(); - for (const SavedLink &link : linksToSave(diagram() && diagram()->project() - ? diagram()->project()->waitingLinkTargets() : QSet())) - links.append({link.uuid, link.group_index}); + for (Element *elmt : std::as_const(connected_elements)) + links.append({elmt->uuid(), m_group_index_map.value(elmt, -1)}); } if (!links.isEmpty()) { QDomElement links_uuids = document.createElement(QStringLiteral("links_uuids")); @@ -1817,10 +1815,14 @@ QList > Element::AlignedFreeTerminals() const /** @brief Element::initLink - Initialise the link between this element and other elements. - This method can be call once because init the link according to - uuid store in a private list, after link, the list is clear, so - call another time do nothing. + Link this symbol, just read, to its partners already built. Once the + project database holds the link rows, the partners are those its rows + name (QETZ-DB-FIRST stage 2a-3): its own rows, in their order, then the + symbols whose rows name it, then any its file names that the rows miss. + A partner not built yet (lazy folios) is linked when it is, from the + rows, which keep it until then. While a project opens, before the + database is filled, the partners are those its file names, and the list + is kept for the fill. @param prj : ownership project of this element and other element to be linked @@ -1830,88 +1832,84 @@ void Element::initLink(QETProject *prj) // if nothing to link return now if (tmp_uuids_link.isEmpty()) return; + projectDataBase *database = prj->dataBase(); + const bool from_rows = database->holdsLinks(); + QList links; + QSet listed; + const QList> rows = from_rows ? database->partnersToLink(uuid()) + : QList>(); + const QList> own_rows = from_rows ? database->linkRowsOf(uuid()) + : QList>(); + //Its own rows first, then its file's list, then the symbols whose + //rows name it: a partner's row gives no contact group, so it + //comes after the lists that do + for (const auto &row : own_rows) { + if (!listed.contains(row.first)) { + listed.insert(row.first); + links << LinkInfo(row.first, row.second); + } + } + for (const auto &link_info : std::as_const(tmp_uuids_link)) { + if (!listed.contains(link_info.uuid)) { + listed.insert(link_info.uuid); + links << link_info; + } + } + for (const auto &row : rows) { + if (!listed.contains(row.first)) { + listed.insert(row.first); + links << LinkInfo(row.first, row.second); + } + } + //The symbols built: on the folios built, and those built alone as - //partners on folios not built (Diagram::buildSymbols()). A partner - //not built yet (lazy folios) is linked when it is, and the - //saved list is kept until then, for the order it gives + //partners on folios not built (Diagram::buildSymbols()) ElementProvider ep(prj->folios()); QList uuids; - for (const auto &linkInfo : tmp_uuids_link) { + for (const auto &linkInfo : std::as_const(links)) { uuids.append(linkInfo.uuid); } QList elements = ep.fromUuids(uuids); - for (int i = 0; i < tmp_uuids_link.size(); ++i) { + for (const auto &link_info : std::as_const(links)) { for (Element *elmt : elements) { - if (elmt->uuid() == tmp_uuids_link[i].uuid) { + if (elmt->uuid() == link_info.uuid) { elmt->linkToElement(this); - if (tmp_uuids_link[i].group_index >= 0) { - m_group_index_map[elmt] = tmp_uuids_link[i].group_index; + if (link_info.group_index >= 0) { + m_group_index_map[elmt] = link_info.group_index; } break; } } } - restoreSavedLinkOrder(); - if (pendingLinks().isEmpty() || !prj->unloadedFolioCount()) { + restoreLinkOrder(uuids); + if (from_rows) { + //Each partner puts its partners back in the order of its rows: + //this one was linked last + for (Element *partner : std::as_const(connected_elements)) { + QList order; + for (const auto &row : database->partnersToLink(partner->uuid())) + order << row.first; + partner->restoreLinkOrder(order); + } tmp_uuids_link.clear(); } + //Otherwise the project opens: the list is kept for the database's + //fill, which takes its order and its partners not built from it + //(QETProject::readProjectXml() drops it after) } /** - @brief Element::linksToSave - The partners this symbol saves, in the order it saves them: the order - it was saved with (tmp_uuids_link), then partners linked since. A - partner on a folio not built yet (lazy folios), which this symbol will - be linked to, is in the list too: saving needs no partner built (stage - D5a). toXml() writes this list, and the project database's link rows - are written from it, so the two are the same (QETZ-DB-FIRST stage 2a). - @param waiting the symbols on folios not built: a saved partner among - them is kept (QETProject::symbolsOnUnbuiltFolios()) - @return each partner's uuid, its contact group (-1 if none), and the - partner itself when it is built (nullptr for one waiting) + @brief Element::savedLinkList + @return the links this symbol was read with and keeps (initLink()): + each partner's uuid and contact group (-1 if none), in its file's order */ -QList Element::linksToSave(const QSet &waiting) const +QList> Element::savedLinkList() const { - QList links; - if (keepsSavedLinks() && diagram() && diagram()->project()) { - QSet written; - for (const auto &link_info : std::as_const(tmp_uuids_link)) { - Element *linked = nullptr; - for (Element *elmt : std::as_const(connected_elements)) - if (elmt->uuid() == link_info.uuid && !written.contains(elmt)) { linked = elmt; break; } - if (linked) { - written.insert(linked); - links.append({linked->uuid(), m_group_index_map.value(linked, -1), linked}); - } else if (waiting.contains(link_info.uuid)) { - links.append({link_info.uuid, link_info.group_index, nullptr}); - } - } - for (Element *elmt : std::as_const(connected_elements)) - if (!written.contains(elmt)) - links.append({elmt->uuid(), m_group_index_map.value(elmt, -1), elmt}); - } else { - for (Element *elmt : std::as_const(connected_elements)) - links.append({elmt->uuid(), m_group_index_map.value(elmt, -1), elmt}); - } - return links; -} - -/** - @brief Element::pendingLinks - @return the uuids of the partners this symbol was saved linked to and - is not linked to yet: those on a folio not built yet (lazy folios), - until initLink() runs again once it is -*/ -QSet Element::pendingLinks() const -{ - QSet pending; + QList> list; for (const auto &link_info : tmp_uuids_link) { - pending.insert(link_info.uuid); + list.append({link_info.uuid, link_info.group_index}); } - for (Element *elmt : connected_elements) { - pending.remove(elmt->uuid()); - } - return pending; + return list; } /** @@ -1938,41 +1936,27 @@ void Element::initLink(const QList &candidates) } } } - restoreSavedLinkOrder(); + QList order; + for (const auto &link_info : std::as_const(tmp_uuids_link)) + order << link_info.uuid; + restoreLinkOrder(order); tmp_uuids_link.clear(); } /** - @brief Element::waitingLinks - @return pendingLinks() in the order this symbol was saved with them, - each with its group index (-1 for none) + @brief Element::restoreLinkOrder + Put connected_elements back in the order @p order gives (the order this + symbol saves its partners in). Partners link in the order the folios' + items are visited, and for items at the same height that order changes + from run to run, so a master's contacts were saved in a different order + each time. Linked elements missing from @p order keep their place after it. */ -QList> Element::waitingLinks() const -{ - const QSet pending = pendingLinks(); - QList> list; - for (const auto &link_info : tmp_uuids_link) { - if (pending.contains(link_info.uuid)) { - list.append({link_info.uuid, link_info.group_index}); - } - } - return list; -} - -/** - @brief Element::restoreSavedLinkOrder - Put connected_elements back in the order tmp_uuids_link saved them. - Partners link in the order the folios' items are visited, and for - items at the same height that order changes from run to run, so a - master's contacts were saved in a different order each time. - Linked elements missing from the saved list keep their place after it. -*/ -void Element::restoreSavedLinkOrder() +void Element::restoreLinkOrder(const QList &order) { QList ordered; - for (const auto &link_info : std::as_const(tmp_uuids_link)) { + for (const QUuid &uuid : order) { for (Element *elmt : std::as_const(connected_elements)) { - if (elmt->uuid() == link_info.uuid && !ordered.contains(elmt)) { + if (elmt->uuid() == uuid && !ordered.contains(elmt)) { ordered << elmt; break; } diff --git a/sources/qetgraphicsitem/element.h b/sources/qetgraphicsitem/element.h index bf16816ba..22384b9d9 100644 --- a/sources/qetgraphicsitem/element.h +++ b/sources/qetgraphicsitem/element.h @@ -226,16 +226,12 @@ class Element : public QetGraphicsItem virtual void unlinkAllElements() {} virtual void unlinkElement(Element *) {} virtual void initLink(QETProject *); - QSet pendingLinks() const; - /// A partner as the symbol saves it (linksToSave()) - struct SavedLink { - QUuid uuid; - int group_index = -1; - Element *built = nullptr; ///< nullptr: on a folio not built yet - }; - QList linksToSave(const QSet &waiting) const; - bool keepsSavedLinks() const {return !tmp_uuids_link.isEmpty();} - QList> waitingLinks() const; + /// The links this symbol was read with (its file's links_uuids), + /// until it is linked: the project database writes its link rows + /// from them while it holds none for it (opening) + QList> savedLinkList() const; + void dropSavedLinks() {tmp_uuids_link.clear();} + void restoreLinkOrder(const QList &order); /** Resolve tmp_uuids_link against a caller-supplied candidate list instead of a project-wide search (bugtracker #607). @@ -359,7 +355,6 @@ class Element : public QetGraphicsItem QList plcTablePositions() const { return m_plc_table_positions; } private: - void restoreSavedLinkOrder(); void storeInformation(); projectDataBase *placedStore() const; bool m_must_highlight = false; diff --git a/sources/qetgraphicsitem/terminal.cpp b/sources/qetgraphicsitem/terminal.cpp index ed29d86d9..ab7808359 100644 --- a/sources/qetgraphicsitem/terminal.cpp +++ b/sources/qetgraphicsitem/terminal.cpp @@ -19,6 +19,7 @@ #include "../ElementsCollection/terminaluuids.h" #include "../qet.h" #include "../qetproject.h" +#include "../dataBase/projectdatabase.h" #include "../conductorautonumerotation.h" #include "../diagram.h" #include "../undocommand/addgraphicsobjectcommand.h" @@ -1104,8 +1105,10 @@ QList relatedPotentialTerminal ( //folio is; not while folios are being read or built, which //would build whole report chains Element *report = terminal->parentElement(); - const QSet waiting = report->pendingLinks(); QETProject *project = report->diagram() ? report->diagram()->project() : nullptr; + const QSet waiting = project && project->unloadedFolioCount() + ? project->dataBase()->waitingPartners(report->uuid(), project->symbolsOnUnbuiltFolios()) + : QSet(); if (!waiting.isEmpty() && project && project->state() != QETProject::ProjectParsingRunning) { project->buildFoliosHoldingSymbols(waiting); diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index 23034f30e..b962026a5 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -825,15 +825,6 @@ void QETProject::buildFolios(const QList &folios) const if (!signals_were_blocked && !built.isEmpty()) { emit self->m_data_base.dataBaseUpdated(); } - //A symbol built before still keeping the links it was saved with - //(a partner on a folio not built): its initLink() links those just - //built, puts its partners back in their saved order and drops what - //is done - for (Diagram *d : builtFolios()) - if (!built.contains(d)) - for (Element *elmt : d->elements()) - if (elmt->keepsSavedLinks()) - elmt->initLink(self); //A linked folio report's texts show a wire's properties: the //report's own wire, else its partner's. A report built alone //first, as a partner (buildPartnerSymbols()), linked before it had @@ -1083,9 +1074,17 @@ void QETProject::buildFolioToShow(Diagram *folio) if (chained.contains(d)) first << d; buildFolios(first); + //The partners its symbols' rows name, not linked: those on a + //folio not built are built alone below (the others are built) QSet partners; - for (Element *elmt : folio->elements()) - partners.unite(elmt->pendingLinks()); + for (Element *elmt : folio->elements()) { + QSet linked; + for (Element *partner : elmt->linkedElements()) + linked.insert(partner->uuid()); + for (const auto &row : m_data_base.linkRowsOf(elmt->uuid())) + if (!linked.contains(row.first)) + partners.insert(row.first); + } const QSet folio_uuids = partners.isEmpty() ? QSet() : m_data_base.folioUuidsOfElements(partners); @@ -1127,12 +1126,6 @@ void QETProject::buildPartnerSymbols(const QList &folios, const QSet< for (auto &text : element->dynamicTextItems()) text->refreshLabelConnection(); } - //A built symbol linked to one of them puts its partners back in - //their saved order - for (Diagram *d : builtFolios()) - for (Element *element : d->elements()) - if (element->keepsSavedLinks()) - element->initLink(this); m_state = state; } @@ -2488,10 +2481,11 @@ QDomDocument QETProject::toXml(QHash *kept_folios, QStringList built = unloadedFolioCount(); QSet waiting; QSet chained; + const QSet unbuilt = kept_folios ? symbolsOnUnbuiltFolios() : QSet(); for (Diagram *diagram : builtFolios()) { if (kept_folios) for (Element *element : diagram->elements()) - waiting.unite(element->pendingLinks()); + waiting.unite(m_data_base.waitingPartners(element->uuid(), unbuilt)); for (Diagram *other : tableChainFolios(diagram)) if (!other->isLoaded()) chained.insert(other); } @@ -2505,7 +2499,6 @@ QDomDocument QETProject::toXml(QHash *kept_folios, QStringList } else { loadFolios(); } - m_waiting_link_targets = unloadedFolioCount() ? symbolsOnUnbuiltFolios() : QSet(); //QET_CHECK_ELEMENT_INFO_STORE=1: say whether the stores of symbol //information, folio title blocks and wire properties agree with every placed symbol, folio and //wire, at each save. A test and debugging aid; it changes nothing. @@ -2693,7 +2686,6 @@ QDomDocument QETProject::toXml(QHash *kept_folios, QStringList << links.mid(0, 5).join(QStringLiteral(", ")); } - m_waiting_link_targets.clear(); return(xml_doc); } @@ -3247,15 +3239,12 @@ bool QETProject::usesElement(const ElementsLocation &location) const /** @brief QETProject::folioXml @return the built folio @p folio written as saving writes it: its - symbols keep the links to partners on folios not built yet - (lazy folios), which Diagram::toXml() alone leaves out + symbols' links are their link rows, partners on folios not built yet + (lazy folios) included (Element::toXml()) */ QDomDocument QETProject::folioXml(Diagram *folio) { - m_waiting_link_targets = unloadedFolioCount() ? symbolsOnUnbuiltFolios() : QSet(); - const QDomDocument xml = folio->toXml(); - m_waiting_link_targets.clear(); - return xml; + return folio->toXml(); } /** @@ -3551,6 +3540,9 @@ void QETProject::readProjectXml(QDomDocument &xml_project) //complete rebuild of it. The content being loaded is the same for all //of them, so a single rebuild once everything is in place is enough. m_data_base.setUpdateBlocked(true); + //The symbols read link from their files' lists until the fill + //below writes the link rows (Element::initLink()) + m_data_base.forgetLinks(); //Load the project-wide properties readProjectPropertiesXml(xml_project); @@ -3634,6 +3626,11 @@ void QETProject::readProjectXml(QDomDocument &xml_project) m_data_base.setUpdateBlocked(false); m_data_base.updateDB(xml_project); m_data_base.endPrefill(unloadedFolioCount() > 0); + //The link rows now hold the partners on folios not built: a + //symbol keeps no list of its own (QETZ-DB-FIRST stage 2a-3) + for (Diagram *diagram : builtFolios()) + for (Element *element : diagram->elements()) + element->dropSavedLinks(); //Saving may write the folios not built as they were read, when //building them would write the same: a file written by this //version, whose database rows came from it (toXml()). A .qet does diff --git a/sources/qetproject.h b/sources/qetproject.h index 437112fcf..55c2d8c90 100644 --- a/sources/qetproject.h +++ b/sources/qetproject.h @@ -144,9 +144,6 @@ class QETProject : public QObject void buildFoliosHoldingSymbols(const QSet &symbols); void buildFoliosHoldingTables(); QSet symbolsOnUnbuiltFolios() const; - /// While saving: the symbols on folios not built a built symbol - /// may be saved linked to (Element::toXml()) - QSet waitingLinkTargets() const {return m_waiting_link_targets;} QDomDocument folioXml(Diagram *folio); QSet tableChainFolios(Diagram *folio) const; void loadFolios() const; @@ -588,7 +585,6 @@ class QETProject : public QObject /// Saving a .qetz may copy the part of a folio not built /// (toXml(), stage D4) bool m_kept_parts_trusted = false; - QSet m_waiting_link_targets; /// The entries of the .qetz opened, and the part each folio /// was read from, in order QHash m_container_files; diff --git a/sources/wiringlistexport.cpp b/sources/wiringlistexport.cpp index 8de281782..87fde002a 100644 --- a/sources/wiringlistexport.cpp +++ b/sources/wiringlistexport.cpp @@ -90,9 +90,11 @@ QDomDocument WiringListExport::projectDocument() const //Conductor::toXml(): the label of the last of the symbol's partners. //A symbol still waiting for a partner not built has its links as //saved, in the database - auto linked = [&built, database, &label](const QUuid &uuid) -> std::optional { + const QSet waiting = m_project->unloadedFolioCount() ? m_project->symbolsOnUnbuiltFolios() + : QSet(); + auto linked = [&built, database, &label, &waiting](const QUuid &uuid) -> std::optional { Element *element = built.value(uuid); - if (element && element->pendingLinks().isEmpty()) { + if (element && database->waitingPartners(uuid, waiting).isEmpty()) { if (element->linkedElements().isEmpty()) return std::nullopt; return element->linkedElements().last()->actualLabel(); } diff --git a/tests/qttest/tst_resaveunchanged.cpp b/tests/qttest/tst_resaveunchanged.cpp index 2613e8699..37b91bce5 100644 --- a/tests/qttest/tst_resaveunchanged.cpp +++ b/tests/qttest/tst_resaveunchanged.cpp @@ -522,6 +522,97 @@ private slots: QCOMPARE(dangling, 0); } + // Each placed symbol's saved links in the .qetz @p path, as "symbol + // partner" lines, sorted: the link table of its database + static QStringList qetzLinks(const QString &path) + { + QList entries; + if (!QetZip::fromBytes(read(path), &entries)) return {QStringLiteral("not a zip")}; + QByteArray database; + for (const QetZip::Entry &entry : std::as_const(entries)) + if (entry.name == QLatin1String("project.sqlite")) database = entry.data; + sqlite3 *db = nullptr; + sqlite3_open(":memory:", &db); + unsigned char *copy = static_cast(sqlite3_malloc64(sqlite3_uint64(database.size()))); + memcpy(copy, database.constData(), size_t(database.size())); + sqlite3_deserialize(db, "main", copy, database.size(), database.size(), SQLITE_DESERIALIZE_FREEONCLOSE); + sqlite3_stmt *list = nullptr; + QStringList links; + sqlite3_prepare_v2(db, "SELECT element_uuid, linked_uuid FROM link", -1, &list, nullptr); + while (list && sqlite3_step(list) == SQLITE_ROW) + links << QString::fromUtf8(reinterpret_cast(sqlite3_column_text(list, 0))) + + QLatin1Char(' ') + + QString::fromUtf8(reinterpret_cast(sqlite3_column_text(list, 1))); + sqlite3_finalize(list); + sqlite3_close(db); + links.sort(); + return links; + } + + // Removing a symbol or a folio and undoing it, with lazy folios, gives + // back every link, also those to symbols on folios not built, which + // only the project database's link rows hold (QETZ-DB-FIRST stage + // 2a-3). m_000 as a .qetz saved with every folio built; a contact of + // its middle folio, its folio, or its coil's folio (not built: the coil + // is built alone) removed, then undone. + void undoKeepsLinksToUnbuiltFolios_data() + { + QTest::addColumn("edit"); + QTest::newRow("symbol") << QStringLiteral("qet.deleteElement(f, u)"); + QTest::newRow("folio") << QStringLiteral("qet.removeFolio(f)"); + QTest::newRow("coil folio") << QStringLiteral("qet.removeFolio(folio_of[m])"); + } + void undoKeepsLinksToUnbuiltFolios() + { +#ifndef QET_HAS_SCRIPTING + QSKIP("needs --run: this QElectroTech is built without Qt Qml"); +#endif + QFETCH(QString, edit); + //Each symbol's folio, from the file: asking QElectroTech would + //build the folios + QFile source(QStringLiteral(QET_EXAMPLES_DIR "/m_000.qet")); + QVERIFY(source.open(QIODevice::ReadOnly)); + const QStringList folios = QString::fromUtf8(source.readAll()).split(QStringLiteral("]*\\buuid=\"(\\{[0-9a-f-]+\\})\"")); + for (int i = 1 ; i < folios.size() ; ++i) { + auto it = placed.globalMatch(folios.at(i)); + while (it.hasNext()) folio_of << QStringLiteral("'%1': %2").arg(it.next().captured(1)).arg(i - 1); + } + const QString qetz = m_dir.filePath(QStringLiteral("undo%1.qetz").arg(m_run)); + QVERIFY(!runChecked({QStringLiteral("--resave"), + QStringLiteral(QET_EXAMPLES_DIR "/m_000.qet"), qetz}, true).isEmpty()); + auto save = [&](const QString &name, const QString &body) { + const QString saved = m_dir.filePath(name + QString::number(m_run) + QStringLiteral(".qetz")); + const QString js = m_dir.filePath(name + QString::number(m_run) + QStringLiteral(".js")); + QFile script(js); + if (!script.open(QIODevice::WriteOnly)) return std::pair(); + script.write(QStringLiteral( + "var folio_of = {%3};\n" + "var n = qet.folioCount(), f = -1, u = '', m = '';\n" + "for (var k = 0 ; k < n && !u ; ++k) {\n" + " var g = (Math.floor(n / 2) + k) % n;\n" + " var all = qet.elementUuids(g).slice().sort();\n" + " for (var i = 0 ; i < all.length && !u ; ++i)\n" + " if (qet.elementLinkType(g, all[i]) == 'slave' && qet.linkedElements(g, all[i]).length) { f = g; u = all[i]; m = qet.linkedElements(g, u)[0]; }\n" + "}\n" + "%1\n" + "qet.save('%2');\n").arg(body, saved, folio_of.join(QStringLiteral(", "))).toUtf8()); + script.close(); + return std::pair(saved, runChecked({QStringLiteral("--run"), js, qetz}, false, + "QET_CHECK_LINK_ROWS")); + }; + const auto plain = save(QStringLiteral("undo-plain"), QStringLiteral("qet.log('FOUND ' + !!u);")); + QVERIFY2(plain.second.contains(QStringLiteral("FOUND true")), qPrintable(plain.second.right(600))); + const auto undone = save(QStringLiteral("undo-undone"), + QStringLiteral("qet.log('UNDONE ' + (%1) + ' ' + qet.undo());").arg(edit)); + QVERIFY2(undone.second.contains(QStringLiteral("UNDONE true true")), qPrintable(undone.second.right(600))); + QVERIFY2(undone.second.contains(QStringLiteral("link rows: 0 differ")), qPrintable(undone.second.right(600))); + const QStringList before = qetzLinks(plain.first); + QVERIFY(before.size() > 10); + QCOMPARE(qetzLinks(undone.first), before); + } + // The stores filled from the document before the folios are built // hold what building them gives, for a // project saved by this version (every symbol, folio and wire carries