diff --git a/sources/TerminalStrip/terminalstrip.cpp b/sources/TerminalStrip/terminalstrip.cpp index fe07d959b..337f56766 100644 --- a/sources/TerminalStrip/terminalstrip.cpp +++ b/sources/TerminalStrip/terminalstrip.cpp @@ -1011,6 +1011,15 @@ bool TerminalStrip::fromXml(QDomElement &xml_element) auto xml_layout = xml_element.firstChildElement(QStringLiteral("layout")); if (!xml_layout.isNull()) { + //The folios holding its terminals are built (QET_LAZY_FOLIOS): + //a strip is read with the project, before the database could + //tell where any symbol is + QSet terminals; + const QDomNodeList xml_reals = xml_layout.elementsByTagName(RealTerminal::xmlTagName()); + for (int i = 0 ; i < xml_reals.size() ; ++i) + terminals.insert(QUuid(xml_reals.at(i).toElement().attribute(QStringLiteral("element_uuid")))); + m_project->buildFoliosHoldingSymbols(terminals); + //Get all free elements terminal of the project const ElementProvider ep(m_project); auto free_terminals = ep.freeTerminal(); diff --git a/sources/autoNum/autonumschemecommand.cpp b/sources/autoNum/autonumschemecommand.cpp index 259eb9a3a..7eb96d3dd 100644 --- a/sources/autoNum/autonumschemecommand.cpp +++ b/sources/autoNum/autonumschemecommand.cpp @@ -105,7 +105,9 @@ QList AutoNumSchemeCommand::usersOf(const QETProject *project, Kind k if (!project || title.isEmpty()) { return users; } - for (Diagram *diagram : project->diagrams()) { + //The schemes a folio uses are its own data, read on opening: + //folios() builds nothing + for (Diagram *diagram : project->folios()) { if (kind == Kind::Conductor) { if (diagram->conductorsAutonumName() == title) { users << diagram; @@ -234,7 +236,8 @@ void AutoNumSchemeCommand::rename(const QString &from, const QString &to) if (m_project->conductorCurrentAutoNum() == from) { m_project->setCurrentConductorAutoNum(to); } - for (Diagram *d : m_project->diagrams()) { + //Folios' own data, read on opening: folios() builds nothing + for (Diagram *d : m_project->folios()) { //The highest folio number given, kept by scheme title for (auto *hash : {&d->m_cnd_unitfolio_max, &d->m_cnd_tenfolio_max, &d->m_cnd_hundredfolio_max}) { if (hash->contains(from)) { diff --git a/sources/autoNum/elementautonumschemecommand.cpp b/sources/autoNum/elementautonumschemecommand.cpp index cd3684837..4fedb778e 100644 --- a/sources/autoNum/elementautonumschemecommand.cpp +++ b/sources/autoNum/elementautonumschemecommand.cpp @@ -369,10 +369,19 @@ ElementAutoNumSchemeCommand::NumberSupport ElementAutoNumSchemeCommand::numberSu std::optional ElementAutoNumSchemeCommand::numberOf(const NumberSupport &support, const Element *element) { - if (!support.supported || !element) { + if (!element) { + return std::nullopt; + } + return numberOf(support, element->sequenceStruct()); +} + +/// @return the number the sequential numbers @p seq give, if the scheme knows it +std::optional ElementAutoNumSchemeCommand::numberOf(const NumberSupport &support, + const autonum::sequentialNumbers &seq) +{ + if (!support.supported) { return std::nullopt; } - const autonum::sequentialNumbers seq = element->sequenceStruct(); const QStringList &list = support.type == QLatin1String("unit") ? seq.unit : support.type == QLatin1String("ten") ? seq.ten : seq.hundred; @@ -385,14 +394,18 @@ std::optional ElementAutoNumSchemeCommand::numberOf(const NumberSupport &su } namespace { -/// The elements of @p project which follow the scheme @p title and are not slaves or reports -QVector numberedFollowers(const QETProject *project, const QString &title) +/// The symbols of @p project which follow the scheme @p title and are not +/// slaves or reports, built or not (QETProject::symbolNumbering()) +QList numberedFollowers(const QETProject *project, const QString &title) { - QVector list; - const auto followers = project->elementsUsingElementAutoNum(title); - for (Element *el : followers) { - if (el->linkType() != Element::Slave && !(el->linkType() & Element::AllReport)) { - list << el; + QList list; + const QUuid id = project->elementAutoNumId(title); + if (id.isNull()) { + return list; + } + for (const auto &symbol : project->symbolNumbering()) { + if (symbol.numbered && !symbol.formula.isEmpty() && symbol.scheme == id) { + list << symbol; } } return list; @@ -411,13 +424,10 @@ QSet ElementAutoNumSchemeCommand::labelsHeldBesides(const QETProject *p if (!project) { return labels; } - for (Diagram *d : project->diagrams()) { - for (QGraphicsItem *item : d->items()) { - if (auto *el = qgraphicsitem_cast(item)) { - if (el != element) { - labels << el->elementInformations().value(QETInformation::ELMT_LABEL).toString(); - } - } + //Folios not built yet answer from what building them would give + for (const auto &symbol : project->symbolNumbering()) { + if (symbol.element != element) { + labels << symbol.label; } } labels.remove(QString()); @@ -453,8 +463,8 @@ std::optional ElementAutoNumScheme CounterConflict conflict; conflict.counter = counter; - for (const Element *el : numberedFollowers(project, title)) { - if (const auto number = numberOf(support, el)) { + for (const auto &symbol : numberedFollowers(project, title)) { + if (const auto number = numberOf(support, symbol.sequence)) { if (*number >= counter) { ++conflict.count; conflict.highest = std::max(conflict.highest, *number); @@ -486,8 +496,8 @@ QList ElementAutoNumSchemeCommand::gapRan return ranges; } QSet used; - for (const Element *el : numberedFollowers(project, title)) { - if (const auto number = numberOf(support, el)) { + for (const auto &symbol : numberedFollowers(project, title)) { + if (const auto number = numberOf(support, symbol.sequence)) { used << *number; } } @@ -559,22 +569,19 @@ QList ElementAutoNumSchemeCommand::freeNumbers(const QETProject *project, QSet used; int highest = context.itemAt(support.partIndex).value(1).toInt(); - for (const Element *el : numberedFollowers(project, title)) { - if (el == element) continue; - if (const auto number = numberOf(support, el)) { + for (const auto &symbol : numberedFollowers(project, title)) { + if (symbol.element == element) continue; + if (const auto number = numberOf(support, symbol.sequence)) { used << *number; highest = std::max(highest, *number); } } QSet labels; - for (Diagram *d : project->diagrams()) { - for (QGraphicsItem *item : d->items()) { - if (auto *el = qgraphicsitem_cast(item)) { - if (el != element) { - labels << el->elementInformations().value(QETInformation::ELMT_LABEL).toString(); - } - } + //Folios not built yet answer from what building them would give + for (const auto &symbol : project->symbolNumbering()) { + if (symbol.element != element) { + labels << symbol.label; } } labels.remove(QString()); diff --git a/sources/autoNum/elementautonumschemecommand.h b/sources/autoNum/elementautonumschemecommand.h index c0a69838c..5ed93e0aa 100644 --- a/sources/autoNum/elementautonumschemecommand.h +++ b/sources/autoNum/elementautonumschemecommand.h @@ -18,6 +18,7 @@ #ifndef ELEMENTAUTONUMSCHEMECOMMAND_H #define ELEMENTAUTONUMSCHEMECOMMAND_H +#include "assignvariables.h" #include "numerotationcontext.h" #include "renumberelementscommand.h" @@ -201,6 +202,8 @@ class ElementAutoNumSchemeCommand : public QUndoCommand static QSet labelsHeldBesides(const QETProject *project, const Element *element); static NumberSupport numberSupport(const NumerotationContext &context); static std::optional numberOf(const NumberSupport &support, const Element *element); + static std::optional numberOf(const NumberSupport &support, + const autonum::sequentialNumbers &seq); static QList gapRanges(const QETProject *project, const QString &title); static QList freeNumbers(const QETProject *project, const QString &title, const Element *element, int limit = 500); diff --git a/sources/autoNum/ui/autonumberingmanagementw.cpp b/sources/autoNum/ui/autonumberingmanagementw.cpp index 93b09fdd2..81225b233 100644 --- a/sources/autoNum/ui/autonumberingmanagementw.cpp +++ b/sources/autoNum/ui/autonumberingmanagementw.cpp @@ -175,7 +175,8 @@ void AutoNumberingManagementW::on_m_apply_folios_rb_clicked() if (ui->m_from_folios_cb->count()<=0) { ui->m_from_folios_cb->clear(); ui->m_from_folios_cb->addItem(""); - foreach (Diagram *diagram, project_->diagrams()){ + //Titles and positions only: folios() builds nothing + foreach (Diagram *diagram, project_->folios()){ if (diagram->title() != "") ui->m_from_folios_cb->addItem( diagram->title(), @@ -206,16 +207,17 @@ void AutoNumberingManagementW::on_m_from_folios_cb_currentIndexChanged( if (index > 0) { ui->m_to_folios_cb->setEnabled(true); ui->m_to_folios_cb->addItem(""); - for (int i=index;idiagrams().size();i++) { - if (project_->diagrams().at(i)->title() != "") { + const QList folios = project_->folios(); + for (int i=index;ititle() != "") { ui->m_to_folios_cb->addItem( - project_->diagrams().at(i)->title(), - project_->diagrams().at(i)->folioIndex()); + folios.at(i)->title(), + folios.at(i)->folioIndex()); } else ui->m_to_folios_cb->addItem( QString::number( - project_->diagrams().at(i)->folioIndex()), - project_->diagrams().at(i)->folioIndex()); + folios.at(i)->folioIndex()), + folios.at(i)->folioIndex()); } applyEnable(true); ui->m_selected_folios_le->clear(); diff --git a/sources/autoNum/ui/folioautonumbering.cpp b/sources/autoNum/ui/folioautonumbering.cpp index ab477f287..e5d1790bc 100644 --- a/sources/autoNum/ui/folioautonumbering.cpp +++ b/sources/autoNum/ui/folioautonumbering.cpp @@ -136,7 +136,8 @@ void FolioAutonumberingW::on_m_autonumber_tabs_rb_clicked() if (ui->m_from_cb->count()<=0){ ui->m_from_cb->clear(); ui->m_from_cb->addItem(""); - foreach (Diagram *diagram, project_->diagrams()){ + //Titles only: folios() builds nothing + foreach (Diagram *diagram, project_->folios()){ ui->m_from_cb->addItem(diagram->title()); } } @@ -164,9 +165,9 @@ void FolioAutonumberingW::on_m_from_cb_currentIndexChanged(int) ui->m_to_cb->clear(); if (index > 0){ ui->m_to_cb->setEnabled(true); - for (int i=index;idiagrams().size();i++) - ui->m_to_cb->addItem(project_ - ->diagrams().at(i)->title()); + const QList folios = project_->folios(); + for (int i=index;im_to_cb->addItem(folios.at(i)->title()); applyEnable(true); } else{ diff --git a/sources/bordertitleblock.cpp b/sources/bordertitleblock.cpp index 6ab3e0171..f0e6f65c1 100644 --- a/sources/bordertitleblock.cpp +++ b/sources/bordertitleblock.cpp @@ -29,6 +29,10 @@ #include "titleblocktemplaterenderer.h" +#include "diagram.h" +#include "qetproject.h" +#include "dataBase/projectdatabase.h" + #include #include #include @@ -282,28 +286,98 @@ void BorderTitleBlock::borderFromXml(const QDomElement &xml_elmt) { @return the properties of the titleblock \~French les proprietes du cartouches */ -TitleBlockProperties BorderTitleBlock::exportTitleBlock() +TitleBlockProperties BorderTitleBlock::exportTitleBlock() const +{ + return current(); +} + +/** + @brief BorderTitleBlock::localTitleBlock + @return the properties of the titleblock this object holds itself, + whether or not its folio is placed in a project +*/ +TitleBlockProperties BorderTitleBlock::localTitleBlock() const { TitleBlockProperties ip; - ip.author = author(); - ip.date = date(); - ip.title = title(); - ip.filename = fileName(); - ip.plant = plant(); - ip.locmach = locmach(); - ip.indexrev = indexrev(); - ip.version = version(); - ip.folio = folio(); + ip.author = btb_author_; + ip.date = btb_date_; + ip.title = btb_title_; + ip.filename = btb_filename_; + ip.plant = btb_plant_; + ip.locmach = btb_locmach_; + ip.indexrev = btb_indexrev_; + ip.version = btb_version_; + ip.folio = btb_folio_; ip.template_name = titleBlockTemplateName(); ip.display_at = m_edge; - ip.auto_page_num = autoPageNum(); + ip.auto_page_num = btb_auto_page_num_; ip.context = additional_fields_; ip.collection = QET::QetCollection::Embedded; return(ip); } +/** + @brief BorderTitleBlock::current + @return the properties of the titleblock: for a folio placed in a + project the project's store holds them (DB-ACCESSORS-PLAN.md stage + 4.4), for any other this object's own copy. +*/ +TitleBlockProperties BorderTitleBlock::current() const +{ + if (const TitleBlockProperties *s = stored()) + return *s; + return localTitleBlock(); +} + +/** + @brief BorderTitleBlock::stored + @return the properties the project's store holds for this titleblock's + folio, or nullptr if it is not placed. Read at once: the getters use it + field by field, as label formulas ask for the folio and title of every + symbol's folio, and copying the whole properties each time costs. +*/ +const TitleBlockProperties *BorderTitleBlock::stored() const +{ + if (projectDataBase *store = placedStore()) + return store->storedFolioTitleBlock(m_owner_folio->uuid()); + return nullptr; +} + +/** + @brief BorderTitleBlock::setOwnerFolio + @param folio : the folio this border and title block belongs to +*/ +void BorderTitleBlock::setOwnerFolio(Diagram *folio) +{ + m_owner_folio = folio; +} + +/** + @brief BorderTitleBlock::placedStore + @return the project database holding this titleblock's properties, or + nullptr if its folio is not placed in a project +*/ +projectDataBase *BorderTitleBlock::placedStore() const +{ + QETProject *project = m_owner_folio ? m_owner_folio->project() : nullptr; + projectDataBase *store = project ? project->dataBase() : nullptr; + return store && store->placedFolio(m_owner_folio->uuid()) == m_owner_folio + ? store : nullptr; +} + +/** + @brief BorderTitleBlock::storeTitleBlock + Write this titleblock's properties to its project's store, once its + folio is placed: every change of them comes through here. +*/ +void BorderTitleBlock::storeTitleBlock() +{ + if (projectDataBase *store = placedStore()) + store->storeFolioTitleBlock(m_owner_folio->uuid(), localTitleBlock()); +} + /** @brief BorderTitleBlock::importTitleBlock @param ip the new properties of titleblock @@ -333,6 +407,7 @@ void BorderTitleBlock::importTitleBlock(const TitleBlockProperties &ip) // through setFolioData(), // which in turn calls updateDiagramContextForTitleBlock(). emit(needTitleBlockTemplate(ip.template_name)); + storeTitleBlock(); emit informationChanged(); } @@ -391,6 +466,7 @@ void BorderTitleBlock::setTitleBlockTemplate( const TitleBlockTemplate *titleblock_template) { m_titleblock_template_renderer -> setTitleBlockTemplate( titleblock_template); + storeTitleBlock(); } /** @@ -889,9 +965,21 @@ DiagramPosition BorderTitleBlock::convertPosition(const QPointF &pos) void BorderTitleBlock::setFolio(const QString &folio) { btb_folio_ = folio; + storeTitleBlock(); emit (titleBlockFolioChanged(folio)); } +/** + @brief BorderTitleBlock::setAutoPageNum + @param title : the title of the folio numbering this folio follows, + nothing else changes +*/ +void BorderTitleBlock::setAutoPageNum(const QString &title) +{ + btb_auto_page_num_ = title; + storeTitleBlock(); +} + /** @brief BorderTitleBlock::updateDiagramContextForTitleBlock Update the informations given to the titleblock template @@ -915,9 +1003,10 @@ void BorderTitleBlock::updateDiagramContextForTitleBlock( // So an empty page-level value is skipped only when a real project-level // one is already there to show through; otherwise it still goes in // empty, which is what makes the placeholder resolve to nothing. + const TitleBlockProperties tb = current(); DiagramContext context = initial_context; - foreach (QString key, additional_fields_.keys()) { - const QVariant value = additional_fields_[key]; + foreach (QString key, tb.context.keys()) { + const QVariant value = tb.context[key]; if (!value.toString().isEmpty() || !context.contains(key)) context.addValue(key, value); } @@ -925,20 +1014,20 @@ void BorderTitleBlock::updateDiagramContextForTitleBlock( // ... overridden by the historical and/or dynamically generated fields QLocale var; var.dateFormat(QLocale::ShortFormat); - context.addValue("author", btb_author_); + context.addValue("author", tb.author); context.addValue( "date", - QLocale::system().toString(btb_date_, QLocale::ShortFormat)); - context.addValue("title", btb_title_); - context.addValue("filename", btb_filename_); - context.addValue("plant", btb_plant_); - context.addValue("locmach", btb_locmach_); - context.addValue("indexrev", btb_indexrev_); - context.addValue("version", btb_version_); + QLocale::system().toString(tb.date, QLocale::ShortFormat)); + context.addValue("title", tb.title); + context.addValue("filename", tb.filename); + context.addValue("plant", tb.plant); + context.addValue("locmach", tb.locmach); + context.addValue("indexrev", tb.indexrev); + context.addValue("version", tb.version); context.addValue("folio", btb_final_folio_); context.addValue("folio-id", folio_index_); context.addValue("folio-total", folio_total_); - context.addValue("auto_page_num", btb_auto_page_num_); + context.addValue("auto_page_num", tb.auto_page_num); context.addValue("previous-folio-num", m_previous_folio_num); context.addValue("next-folio-num", m_next_folio_num); @@ -1041,11 +1130,12 @@ void BorderTitleBlock::setFolioData( // regenerate the content of the folio field // regenere le contenu du champ folio - btb_final_folio_ = btb_folio_; + btb_final_folio_ = folio(); if (btb_final_folio_.contains("%autonum")){ btb_final_folio_.replace("%autonum", autonum); btb_folio_ = btb_final_folio_; + storeTitleBlock(); } btb_final_folio_.replace("%id", QString::number(folio_index_)); btb_final_folio_.replace("%total", QString::number(folio_total_)); diff --git a/sources/bordertitleblock.h b/sources/bordertitleblock.h index 8a57d3d9c..adc422752 100644 --- a/sources/bordertitleblock.h +++ b/sources/bordertitleblock.h @@ -29,6 +29,8 @@ class QPainter; class DiagramPosition; class TitleBlockTemplate; class TitleBlockTemplateRenderer; +class Diagram; +class projectDataBase; /** @brief The BorderTitleBlock class This class represents the border and the titleblock which frame a @@ -107,34 +109,36 @@ class BorderTitleBlock : public QObject QRectF outsideBorderRect() const; QRectF insideBorderRect() const; - // methods to get title block basic data + // methods to get title block basic data. A folio placed in a + // project reads them from the project's store + // (DB-ACCESSORS-PLAN.md stage 4.4), any other from its own copy. /// @return the value of the title block "Author" field - QString author() const { return(btb_author_); } + QString author() const { const TitleBlockProperties *s = stored(); return(s ? s->author : btb_author_); } /// @return the value of the title block "Date" field - QDate date() const { return(btb_date_); } + QDate date() const { const TitleBlockProperties *s = stored(); return(s ? s->date : btb_date_); } /// @return the value of the title block "Title" field - QString title() const { return(btb_title_); } + QString title() const { const TitleBlockProperties *s = stored(); return(s ? s->title : btb_title_); } /// @return the value of the title block "Folio" field - QString folio() const { return(btb_folio_); } + QString folio() const { const TitleBlockProperties *s = stored(); return(s ? s->folio : btb_folio_); } /// @return the value of the title block "Folio" field as displayed QString finalfolio() const { return(btb_final_folio_); } /// @return the value of the title block "Plant" field - QString plant() const { return(btb_plant_); } + QString plant() const { const TitleBlockProperties *s = stored(); return(s ? s->plant : btb_plant_); } /// @return the value of the title block "Locmach" field - QString locmach() const { return(btb_locmach_); } + QString locmach() const { const TitleBlockProperties *s = stored(); return(s ? s->locmach : btb_locmach_); } /// @return the value of the revision index block "Folio" field - QString indexrev() const { return(btb_indexrev_); } + QString indexrev() const { const TitleBlockProperties *s = stored(); return(s ? s->indexrev : btb_indexrev_); } /// @return the value of the title block "File" field - QString fileName() const { return(btb_filename_); } + QString fileName() const { const TitleBlockProperties *s = stored(); return(s ? s->filename : btb_filename_); } /// @return the value of the title block Additional Fields - QString version() const { return(btb_version_); } + QString version() const { const TitleBlockProperties *s = stored(); return(s ? s->version : btb_version_); } /// @return the value of the title block Additional Fields DiagramContext additionalFields() const { - return (additional_fields_); } + const TitleBlockProperties *s = stored(); return(s ? s->context : additional_fields_); } /// @return the value of the title block - QString autoPageNum() const { return(btb_auto_page_num_); } + QString autoPageNum() const { const TitleBlockProperties *s = stored(); return(s ? s->auto_page_num : btb_auto_page_num_); } /// The title of the folio numbering this folio follows, nothing else changes - void setAutoPageNum(const QString &title) { btb_auto_page_num_ = title; } + void setAutoPageNum(const QString &title); /// @return the value of the total number of folios int folioTotal() const { return(folio_total_);} @@ -175,7 +179,9 @@ class BorderTitleBlock : public QObject void borderToXml(QDomElement &); void borderFromXml(const QDomElement &); - TitleBlockProperties exportTitleBlock(); + TitleBlockProperties exportTitleBlock() const; + TitleBlockProperties localTitleBlock() const; + void setOwnerFolio(Diagram *folio); void importTitleBlock(const TitleBlockProperties &); BorderProperties exportBorder(); void importBorder(const BorderProperties &); @@ -289,5 +295,10 @@ class BorderTitleBlock : public QObject bool display_rows_ = false; bool display_border_ = false; TitleBlockTemplateRenderer *m_titleblock_template_renderer; + Diagram *m_owner_folio = nullptr; + TitleBlockProperties current() const; + const TitleBlockProperties *stored() const; + projectDataBase *placedStore() const; + void storeTitleBlock(); }; #endif diff --git a/sources/cable/cablemanager.cpp b/sources/cable/cablemanager.cpp index e3728d5af..6bb7854ea 100644 --- a/sources/cable/cablemanager.cpp +++ b/sources/cable/cablemanager.cpp @@ -335,6 +335,10 @@ void CableManager::refreshLabels(QETProject *project) QString label; }; + //Without cables, the folios not built hold nothing this pass + //changes but stale references (QETProject::cableLabelFolios()) + const QList folios = project->cableLabelFolios(); + QHash bindings; for (Cable *cable : project->cables()) { @@ -361,7 +365,7 @@ void CableManager::refreshLabels(QETProject *project) //the core or the link does. QHash conductors_by_uuid; QHash> endings_on_report; - for (Diagram *diagram : project->diagrams()) + for (Diagram *diagram : folios) { if (!diagram) continue; for (Conductor *conductor : diagram->conductors()) @@ -440,7 +444,7 @@ void CableManager::refreshLabels(QETProject *project) } } - for (Diagram *diagram : project->diagrams()) + for (Diagram *diagram : folios) { if (!diagram) continue; for (Conductor *conductor : diagram->conductors()) diff --git a/sources/cli_export.cpp b/sources/cli_export.cpp index b8c2278ea..80e7f1aa4 100644 --- a/sources/cli_export.cpp +++ b/sources/cli_export.cpp @@ -869,8 +869,8 @@ int exportLinks(QETProject &project, const QString &output) /// diff can reveal markup QET silently normalises (tolerated-but-invalid XML). int resaveProject(QETProject &project, const QString &output) { - const QDomDocument doc = project.toXml(); if (QETProject::isContainerPath(output)) { + const QDomDocument doc = project.toXml(); QString error; if (!QETProject::writeContainer(doc, output, &error)) { err << "Cannot write '" << output << "': " << error << "\n"; @@ -879,13 +879,17 @@ int resaveProject(QETProject &project, const QString &output) out << "Re-saved project -> " << output << "\n"; return 0; } + //Folios not built (QET_LAZY_FOLIOS) written as read when that is + //what building them would write (QETProject::toXml()) + QHash kept_folios; + const QDomDocument doc = project.toXml(&kept_folios); QFile file(output); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { err << "Cannot open '" << output << "' for writing.\n"; return 1; } QTextStream fout(&file); - fout << doc.toString(4); + fout << QETProject::spliceKeptFolios(doc.toString(4), kept_folios); file.close(); out << "Re-saved project -> " << output << "\n"; return 0; @@ -963,21 +967,25 @@ int setTitleBlock(QETProject &project, const QString &output, ++folios; } - const QDomDocument doc = project.toXml(); if (QETProject::isContainerPath(output)) { + const QDomDocument doc = project.toXml(); QString error; if (!QETProject::writeContainer(doc, output, &error)) { err << "Cannot write '" << output << "': " << error << "\n"; return 1; } } else { + //Folios not built (QET_LAZY_FOLIOS) written as read when that is + //what building them would write (QETProject::toXml()) + QHash kept_folios; + const QDomDocument doc = project.toXml(&kept_folios); QFile file(output); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { err << "Cannot open '" << output << "' for writing.\n"; return 1; } QTextStream fout(&file); - fout << doc.toString(4); + fout << QETProject::spliceKeptFolios(doc.toString(4), kept_folios); file.close(); } out << "Stamped " << fields.size() << " field(s) on " diff --git a/sources/conductorproperties.cpp b/sources/conductorproperties.cpp index f65a2a340..d312941f3 100644 --- a/sources/conductorproperties.cpp +++ b/sources/conductorproperties.cpp @@ -217,10 +217,22 @@ void SingleLineProperties::drawPen(QPainter *painter, */ void SingleLineProperties::toXml(QDomElement &e) const { - e.setAttribute("ground", hasGround ? "true" : "false"); - e.setAttribute("neutral", hasNeutral ? "true" : "false"); - e.setAttribute("phase", phases); - if (isPen()) e.setAttribute("pen", "true"); + for (const auto &attribute : attributes()) + e.setAttribute(attribute.first, attribute.second); +} + +/** + @brief SingleLineProperties::attributes + @return the attributes toXml() writes, in its order +*/ +QList> SingleLineProperties::attributes() const +{ + QList> a; + a << qMakePair(QStringLiteral("ground"), QString::fromLatin1(hasGround ? "true" : "false")); + a << qMakePair(QStringLiteral("neutral"), QString::fromLatin1(hasNeutral ? "true" : "false")); + a << qMakePair(QStringLiteral("phase"), QString::number(phases)); + if (isPen()) a << qMakePair(QStringLiteral("pen"), QStringLiteral("true")); + return a; } /** @@ -268,50 +280,66 @@ ConductorProperties::~ConductorProperties() */ void ConductorProperties::toXml(QDomElement &e) const { - e.setAttribute("type", typeToString(type)); + for (const auto &attribute : attributes()) + e.setAttribute(attribute.first, attribute.second); +} + +/** + @brief ConductorProperties::attributes + @return the attributes toXml() writes, in the order it writes them: the + project database stores a placed wire's properties as these. +*/ +QList> ConductorProperties::attributes() const +{ + QList> a; + auto add = [&a](const char *name, const QString &value) { + a << qMakePair(QString::fromLatin1(name), value); + }; + add("type", typeToString(type)); if (color != QColor(Qt::black)) - e.setAttribute("color", color.name()); + add("color", color.name()); - e.setAttribute("bicolor", m_bicolor? "true" : "false"); - e.setAttribute("color2", m_color_2.name()); - e.setAttribute("dash-size", QString::number(m_dash_size)); + add("bicolor", m_bicolor? "true" : "false"); + add("color2", m_color_2.name()); + add("dash-size", QString::number(m_dash_size)); if (type == Single) - singleLineProperties.toXml(e); + a << singleLineProperties.attributes(); - e.setAttribute("num", text); - e.setAttribute("text_color", text_color.name()); - e.setAttribute("formula", m_formula); - e.setAttribute("cable", m_cable); + add("num", text); + add("text_color", text_color.name()); + add("formula", m_formula); + add("cable", m_cable); if (!m_cable_uuid.isNull()) { //Only written when there is a cable: a conductor which is no //part of one keeps whatever cable text it had and no reference //at all, so a hand written value is never mistaken for a //generated one. - e.setAttribute("cable_uuid", m_cable_uuid.toString()); - e.setAttribute("cable_slot", QString::number(m_cable_slot)); + add("cable_uuid", m_cable_uuid.toString()); + add("cable_slot", QString::number(m_cable_slot)); } - e.setAttribute("bus", m_bus); - e.setAttribute("function", m_function); - e.setAttribute("tension_protocol", m_tension_protocol); - e.setAttribute("conductor_color", m_wire_color); - e.setAttribute("conductor_section", m_wire_section); - e.setAttribute("numsize", QString::number(text_size)); - e.setAttribute("condsize", QString::number(cond_size)); - e.setAttribute("displaytext", m_show_text); - e.setAttribute("onetextperfolio", m_one_text_per_folio); - e.setAttribute("vertirotatetext", QString::number(verti_rotate_text)); - e.setAttribute("horizrotatetext", QString::number(horiz_rotate_text)); + add("bus", m_bus); + add("function", m_function); + add("tension_protocol", m_tension_protocol); + add("conductor_color", m_wire_color); + add("conductor_section", m_wire_section); + add("numsize", QString::number(text_size)); + add("condsize", QString::number(cond_size)); + add("displaytext", QString::number(m_show_text)); + add("onetextperfolio", QString::number(m_one_text_per_folio)); + add("vertirotatetext", QString::number(verti_rotate_text)); + add("horizrotatetext", QString::number(horiz_rotate_text)); QMetaEnum me = QMetaEnum::fromType(); - e.setAttribute("horizontal-alignment", me.valueToKey(m_horizontal_alignment)); - e.setAttribute("vertical-alignment", me.valueToKey(m_vertical_alignment)); + add("horizontal-alignment", QString::fromLatin1(me.valueToKey(m_horizontal_alignment))); + add("vertical-alignment", QString::fromLatin1(me.valueToKey(m_vertical_alignment))); QString conductor_style = writeStyle(); if (!conductor_style.isEmpty()) - e.setAttribute("style", conductor_style); + add("style", conductor_style); + return a; } diff --git a/sources/conductorproperties.h b/sources/conductorproperties.h index 6bb0fd004..c9cb0df84 100644 --- a/sources/conductorproperties.h +++ b/sources/conductorproperties.h @@ -39,6 +39,7 @@ class SingleLineProperties { bool isPen() const; void draw(QPainter *, QET::ConductorSegmentType, const QRectF &); void toXml(QDomElement &) const; + QList> attributes() const; void fromXml(QDomElement &); void toSettings(QSettings &, const QString & = QString()) const; void fromSettings(QSettings &, const QString & = QString()); @@ -130,6 +131,7 @@ class ConductorProperties // methods void toXml(QDomElement &) const; + QList> attributes() const; void fromXml(QDomElement &); void toSettings(QSettings &, const QString & = QString()) const; void fromSettings(QSettings &, const QString & = QString()); diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index c348454db..d71bedfe4 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -16,6 +16,7 @@ along with QElectroTech. If not, see . */ #include "projectdatabase.h" +#include "../qetversion.h" #include "sqlreadonly.h" @@ -112,10 +113,12 @@ void projectDataBase::updateDB() flushDrawingItems(); flushLinks(); flushElementPositions(); + flushConductorProperties(); emit dataBaseUpdated(); return; } + keepUnbuiltFolioRows(); populateDiagramTable(); populateDiagramInfoTable(); populateElementTable(); @@ -123,11 +126,93 @@ void projectDataBase::updateDB() populateConductorTable(); populateLinkTable(); populateDrawingItemTables(); + dropKeptRows(); + flushConductorProperties(); m_content_changed = false; emit dataBaseUpdated(); } +/** + @brief projectDataBase::keepUnbuiltFolioRows + Before the tables are rebuilt from the folios: keep the element, + element_info, terminal, conductor and link rows of the folios not built + yet (QET_LAZY_FOLIOS). They came from the document, nothing of those + folios can have changed since, and the rebuild puts them back where + each folio comes (restoreKeptRows()). Nothing to do when every folio + is built. +*/ +void projectDataBase::keepUnbuiltFolioRows() +{ + m_kept_folios.clear(); + if (!m_project->unloadedFolioCount()) { + return; + } + //Rows filled from the folios have none for a folio not built: it + //is built (diagrams()) and the tables come from it + if (!m_rows_from_document) { + m_project->diagrams(); + return; + } + for (Diagram *folio : m_project->folios()) + if (!folio->isLoaded()) m_kept_folios.insert(folio); + QSqlQuery query(m_data_base); + query.exec(QStringLiteral("CREATE TEMP TABLE kept_folio (uuid VARCHAR(50) PRIMARY KEY)")); + query.prepare(QStringLiteral("INSERT INTO kept_folio (uuid) VALUES (:uuid)")); + for (Diagram *folio : std::as_const(m_kept_folios)) { + query.bindValue(QStringLiteral(":uuid"), folio->uuid().toString()); + query.exec(); + } + for (const QString &sql : { + QStringLiteral("CREATE TEMP TABLE kept_element AS SELECT * FROM element " + "WHERE diagram_uuid IN (SELECT uuid FROM kept_folio) ORDER BY rowid"), + QStringLiteral("CREATE TEMP TABLE kept_element_info AS SELECT * FROM element_info " + "WHERE element_uuid IN (SELECT uuid FROM kept_element) ORDER BY rowid"), + QStringLiteral("CREATE TEMP TABLE kept_terminal AS SELECT * FROM terminal " + "WHERE element_uuid IN (SELECT uuid FROM kept_element) ORDER BY rowid"), + QStringLiteral("CREATE TEMP TABLE kept_conductor AS SELECT * FROM conductor " + "WHERE diagram_uuid IN (SELECT uuid FROM kept_folio) ORDER BY rowid"), + QStringLiteral("CREATE TEMP TABLE kept_link AS SELECT * FROM link " + "WHERE element_uuid IN (SELECT uuid FROM kept_element) ORDER BY rowid")}) { + if (!query.exec(sql)) { + qDebug() << "projectDataBase::keepUnbuiltFolioRows error : " << query.lastError(); + } + } +} + +/** + @brief projectDataBase::restoreKeptRows + Put back into @p table the rows kept for @p folio, not built + (keepUnbuiltFolioRows()), in the order they had. @p where selects them + in the kept table, with :folio for the folio's uuid. +*/ +void projectDataBase::restoreKeptRows(const QString &table, Diagram *folio, const QString &where) +{ + QSqlQuery query(m_data_base); + query.prepare(QStringLiteral("INSERT INTO %1 SELECT * FROM kept_%1 WHERE %2 ORDER BY rowid") + .arg(table, where)); + query.bindValue(QStringLiteral(":folio"), folio->uuid().toString()); + if (!query.exec()) { + qDebug() << "projectDataBase::restoreKeptRows error : " << table << query.lastError(); + } +} + +/** + @brief projectDataBase::dropKeptRows + The rows kept by keepUnbuiltFolioRows() are back: drop their copies +*/ +void projectDataBase::dropKeptRows() +{ + if (m_kept_folios.isEmpty()) { + return; + } + QSqlQuery query(m_data_base); + for (const char *table : {"kept_folio", "kept_element", "kept_element_info", + "kept_terminal", "kept_conductor", "kept_link"}) + query.exec(QStringLiteral("DROP TABLE %1").arg(QLatin1String(table))); + m_kept_folios.clear(); +} + /** @brief projectDataBase::updateDB updateDB() for a project just read from @p document. @@ -157,7 +242,9 @@ void projectDataBase::updateDB(const QDomDocument &document) why = QStringLiteral("QET_DATABASE_FROM_FOLIOS is set"); } else if (populateFromDocument(document, &why)) { qInfo() << "Project database filled from the document"; + m_rows_from_document = true; populateDrawingItemTables(); + flushConductorProperties(); m_content_changed = false; emit dataBaseUpdated(); return; @@ -289,7 +376,9 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString //The folios, in the order the project read them (readDiagramsXml()), //each with its saved uuid -- the one the built folio has. const QDomNodeList diagram_nodes = document.elementsByTagName(QStringLiteral("diagram")); - const QList diagrams = m_project->diagrams(); + //Built or not (QET_LAZY_FOLIOS): only their uuids are read, and the + //conductors of those built + const QList diagrams = m_project->folios(); if (diagram_nodes.size() != diagrams.size()) { return refuse(QStringLiteral("the folios are not the document's")); } @@ -302,6 +391,7 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString QList diagram_infos; QList diagram_dates; QList diagram_uuids; + QSet unjoined; QList elements; QList conductors; QSet element_uuids; @@ -522,6 +612,9 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString const QUuid terminal(conductor_xml.attribute(QStringLiteral("terminal") + index)); const int e = on_this_folio.value(owner, -1); if (e < 0) { + //Building the folio leaves this wire out and + //says so (Diagram::wiresNotReconnected()) + unjoined.insert(diagram_uuid); found = false; break; } @@ -716,9 +809,68 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString } } } + m_folios_with_unjoined_wires = unjoined; return true; } +/** + @brief projectDataBase::foliosWithUnjoinedWires + @return the uuids of the folios whose document has a wire that building + them will leave out, an end of it not being found: those whose + Diagram::wiresNotReconnected() will not be empty. Known when the + database was last filled from the document, else empty. +*/ +QSet projectDataBase::foliosWithUnjoinedWires() const +{ + return m_folios_with_unjoined_wires; +} + +/** + @brief projectDataBase::elementTypesOnFolios + @return the kind (ElementData::typeToString()) of every symbol on the + folios @p folios, by uuid, read from the element table as it is: for + folios not built yet (QET_LAZY_FOLIOS) its rows came from the document + and nothing can have changed them. A symbol whose definition was not + found has no row. +*/ +QHash projectDataBase::elementTypesOnFolios(const QSet &folios) const +{ + QHash types; + QSqlQuery query(m_data_base); + query.prepare(QStringLiteral("SELECT uuid, type FROM element WHERE diagram_uuid = :folio")); + for (const QUuid &folio : folios) { + query.bindValue(QStringLiteral(":folio"), folio.toString()); + if (!query.exec()) { + continue; + } + while (query.next()) { + types.insert(QUuid(query.value(0).toString()), query.value(1).toString()); + } + } + return types; +} + +/** + @brief projectDataBase::folioUuidsOfElements + @return the uuids of the folios the symbols @p elements are on, read + from the element table as it is, without bringing it up to date: for a + symbol on a folio not built yet (QET_LAZY_FOLIOS) its row came from the + document and nothing can have changed it. +*/ +QSet projectDataBase::folioUuidsOfElements(const QSet &elements) const +{ + QSet folios; + QSqlQuery query(m_data_base); + query.prepare(QStringLiteral("SELECT diagram_uuid FROM element WHERE uuid = :uuid")); + for (const QUuid &uuid : elements) { + query.bindValue(QStringLiteral(":uuid"), uuid.toString()); + if (query.exec() && query.next()) { + folios.insert(QUuid(query.value(0).toString())); + } + } + return folios; +} + /** @brief projectDataBase::setUpdateBlocked @param blocked : whether updateDB() should skip the full rebuild @@ -728,6 +880,26 @@ void projectDataBase::setUpdateBlocked(bool blocked) m_update_blocked = blocked; } +/** + @brief projectDataBase::setBuildingFolio + @see the declaration. The stores still take what building writes. +*/ +void projectDataBase::setBuildingFolio(bool building) +{ + //A folio built while another is (a caller of diagrams() reached + //from building it) nests: only the outermost restores. + if (building) { + if (m_building_folios++ == 0) { + m_changed_before_folio = m_content_changed; + m_blocked_before_folio = m_update_blocked; + m_update_blocked = true; + } + } else if (m_building_folios > 0 && --m_building_folios == 0) { + m_update_blocked = m_blocked_before_folio; + m_content_changed = m_changed_before_folio; + } +} + /** @brief projectDataBase::project @return the project of this database @@ -815,11 +987,23 @@ QSqlQuery projectDataBase::newQuery(const QString &query, QString *error) { //Drawing-item rows are rewritten lazily, see drawingItemChanged(). //Every read from outside comes through here, so this is the one //place the queue has to be emptied for a reader to see current rows. - //The same goes for link rows and elements' folio cells, see - //linksChanged() and elementMoved(). + //The same goes for link rows, elements' folio cells and wires' + //properties, see linksChanged(), elementMoved() and + //storeConductorProperties(). + //The drawing tables hold what only built items know (a text's + //laid-out size, a picture's pixels): with folios not built yet + //(QET_LAZY_FOLIOS) a read of them builds those first. Every other + //table was filled from the document. + static const QRegularExpression drawing_tables( + QStringLiteral("\\b(shape|independent_text|image)\\b"), + QRegularExpression::CaseInsensitiveOption); + if (m_project && m_project->unloadedFolioCount() && drawing_tables.match(query).hasMatch()) { + m_project->loadFolios(); + } flushDrawingItems(); flushLinks(); flushElementPositions(); + flushConductorProperties(); // First gate: which kind of statement is acceptable here at all. A // textual check is the right tool for that and the wrong tool for @@ -907,21 +1091,13 @@ void projectDataBase::addElement(Element *element) return; } - bindElementValues(m_insert_elements_query, element, element->diagram()); - if (!m_insert_elements_query.exec()) { - qDebug() << "projectDataBase::addElement insert element error : " << m_insert_elements_query.lastError(); - } + placeElement(element); connect(element, &Element::linkedElementChanged, this, &projectDataBase::linksChanged, Qt::UniqueConnection); connect(element, &QGraphicsObject::xChanged, this, &projectDataBase::elementMoved, Qt::UniqueConnection); connect(element, &QGraphicsObject::yChanged, this, &projectDataBase::elementMoved, Qt::UniqueConnection); - - bindElementInfoValues(m_insert_element_info_query, element); - if (!m_insert_element_info_query.exec()) { - qDebug() << "projectDataBase::addElement insert element info error : " << m_insert_element_info_query.lastError(); - } else { - emit dataBaseUpdated(); - } + writeElementRows(element); + emit dataBaseUpdated(); } /** @@ -931,32 +1107,14 @@ void projectDataBase::addElement(Element *element) void projectDataBase::removeElement(Element *element) { m_content_changed = true; - bool changed = false; - - m_remove_element_query.bindValue(":uuid", element->uuid().toString()); - if (m_remove_element_query.exec()) { - changed = true; - } else { - qDebug() << "projectDataBase::removeElement remove error : " << m_remove_element_query.lastError(); - } - - m_remove_element_info_query.bindValue(":uuid", element->uuid().toString()); - if (m_remove_element_info_query.exec()) { - changed = true; - } else { - qDebug() << "projectDataBase::removeElement remove element_info error : " << m_remove_element_info_query.lastError(); - } - - QSqlQuery remove_links(m_data_base); - remove_links.prepare(QStringLiteral("DELETE FROM link WHERE element_uuid = :uuid OR linked_uuid = :uuid")); - remove_links.bindValue(QStringLiteral(":uuid"), element->uuid().toString()); - if (!remove_links.exec()) { - qDebug() << "projectDataBase::removeElement remove link error : " << remove_links.lastError(); - } - - if (changed) { - emit dataBaseUpdated(); + const QUuid uuid = element->uuid(); + removeElementRows(uuid); + unplaceElement(element, uuid); + //Another symbol still holds the uuid (F100): the rows are its own. + if (Element *other = otherHolder(uuid, element)) { + writeElementRows(other); } + emit dataBaseUpdated(); } /** @@ -998,6 +1156,7 @@ void projectDataBase::elementInfoChanged(QList elements) void projectDataBase::addDiagram(Diagram *diagram) { m_content_changed = true; + placeFolio(diagram); m_insert_diagram_query.bindValue(":uuid", diagram->uuid().toString()); m_insert_diagram_query.bindValue(":pos", m_project->folioIndex(diagram)+1); if(!m_insert_diagram_query.exec()) { @@ -1016,6 +1175,24 @@ void projectDataBase::addDiagram(Diagram *diagram) for (QGraphicsItem *item : items) { addDrawingItem(item); } + //So do its symbols, their wires and the links to and from them + //(F107). + const QList elements = diagram->elements(); + const bool own_transaction = !elements.isEmpty() && m_data_base.transaction(); + //One signal for the folio, not one per symbol and wire: each makes + //every folio table query the database again. + const bool blocked = blockSignals(true); + for (Element *element : elements) { + addElement(element); + queueLinks(element); + for (Element *linked : element->linkedElements()) + queueLinks(linked); + } + for (Conductor *conductor : diagram->conductors()) + addConductor(conductor); + blockSignals(blocked); + if (own_transaction) + m_data_base.commit(); //The folios after the new one moved down, and a folio number made //from %id or %total changed on every folio. @@ -1095,6 +1272,12 @@ void projectDataBase::removeDiagram(Diagram *diagram) { m_content_changed = true; const QString uuid_str = diagram->uuid().toString(); + unplaceFolio(diagram, diagram->uuid()); + for (Conductor *conductor : diagram->conductors()) + unplaceConductor(conductor, conductor->uuid()); + //Its symbols are no longer placed (they live on in the undo stack) + for (Element *element : diagram->elements()) + unplaceElement(element, element->uuid()); //Order matters: element_info and terminal are scoped through a //subquery on element, so they must run before element itself is @@ -1224,6 +1407,24 @@ void projectDataBase::addConductor(Conductor *conductor) return; } + placeConductor(conductor); + watchConductor(conductor); + writeConductorRow(conductor); + emit dataBaseUpdated(); +} + +/** + @brief projectDataBase::writeConductorRow + Write the conductor row of @p conductor, a placed wire, and the rows of + the terminals it ends on. A uuid that already has a row keeps it, as in + a full rebuild; nothing is written while updates are blocked (the + rebuild that ends them writes every row). +*/ +void projectDataBase::writeConductorRow(Conductor *conductor) +{ + if (m_update_blocked) { + return; + } //Both endpoints must belong to an element: the terminal table is keyed //on (terminal, element) and a terminal with no parent has no identity //to key on. Terminals whose *definition* predates terminal uuids are @@ -1237,12 +1438,9 @@ void projectDataBase::addConductor(Conductor *conductor) insertTerminal(conductor->terminal1); insertTerminal(conductor->terminal2); - watchConductor(conductor); bindConductorValues(m_insert_conductor_query, conductor, conductor->diagram()); if (!m_insert_conductor_query.exec()) { - qDebug() << "projectDataBase::addConductor insert error : " << m_insert_conductor_query.lastError(); - } else { - emit dataBaseUpdated(); + qDebug() << "projectDataBase::writeConductorRow insert error : " << m_insert_conductor_query.lastError(); } } @@ -1253,6 +1451,27 @@ void projectDataBase::addConductor(Conductor *conductor) void projectDataBase::removeConductor(Conductor *conductor) { m_content_changed = true; + const QUuid uuid = conductor->uuid(); + removeConductorRow(uuid); + unplaceConductor(conductor, uuid); + //Another wire still holds the uuid: the row is its own. + if (Conductor *other = otherConductorHolder(uuid, conductor)) { + writeConductorRow(other); + } + emit dataBaseUpdated(); +} + +/** + @brief projectDataBase::removeConductorRow + Remove the conductor row of @p uuid, and the rows of the terminals it + ended on unless another conductor ends there; nothing while updates are + blocked, as writeConductorRow(). +*/ +void projectDataBase::removeConductorRow(const QUuid &uuid) +{ + if (m_update_blocked) { + return; + } //The terminal table lists the terminals a conductor ends on: the //conductor's two ends go with it unless another conductor ends there. QList> ends; @@ -1260,15 +1479,15 @@ void projectDataBase::removeConductor(Conductor *conductor) read_ends.prepare(QStringLiteral("SELECT terminal1_uuid, terminal1_element_uuid, " "terminal2_uuid, terminal2_element_uuid " "FROM conductor WHERE uuid = :uuid")); - read_ends.bindValue(QStringLiteral(":uuid"), conductor->uuid().toString()); + read_ends.bindValue(QStringLiteral(":uuid"), uuid.toString()); if (read_ends.exec() && read_ends.next()) { ends << qMakePair(read_ends.value(0).toString(), read_ends.value(1).toString()) << qMakePair(read_ends.value(2).toString(), read_ends.value(3).toString()); } - m_remove_conductor_query.bindValue(":uuid", conductor->uuid().toString()); + m_remove_conductor_query.bindValue(":uuid", uuid.toString()); if (!m_remove_conductor_query.exec()) { - qDebug() << "projectDataBase::removeConductor delete error : " << m_remove_conductor_query.lastError(); + qDebug() << "projectDataBase::removeConductorRow delete error : " << m_remove_conductor_query.lastError(); return; } @@ -1282,10 +1501,9 @@ void projectDataBase::removeConductor(Conductor *conductor) remove_end.bindValue(QStringLiteral(":uuid"), end.first); remove_end.bindValue(QStringLiteral(":element_uuid"), end.second); if (!remove_end.exec()) { - qDebug() << "projectDataBase::removeConductor terminal delete error : " << remove_end.lastError(); + qDebug() << "projectDataBase::removeConductorRow terminal delete error : " << remove_end.lastError(); } } - emit dataBaseUpdated(); } /** @@ -1577,7 +1795,7 @@ bool projectDataBase::writeDrawingItem(QObject *object) { auto *item = dynamic_cast(object); auto *diagram = item ? qobject_cast(item->scene()) : nullptr; - if (!diagram || !m_project || !m_project->diagrams().contains(diagram)) { + if (!diagram || !m_project || !m_project->folios().contains(diagram)) { //Not on a folio of this project (any more): nothing to write, //and nothing to wait for. return true; @@ -1693,8 +1911,10 @@ void projectDataBase::populateDrawingItemTables() //Queued directly, not through addDrawingItem(): every one of them //came onto its folio through Diagram::addItem(), which connected it - //already, and a full rebuild runs on every load. - for (auto diagram : m_project->diagrams()) + //already, and a full rebuild runs on every load. A folio not built + //yet (QET_LAZY_FOLIOS) has none: its items write their rows as it + //is built. + for (auto diagram : m_project->folios()) { const QList items = diagram->items(); for (QGraphicsItem *item : items) @@ -1787,6 +2007,50 @@ bool projectDataBase::createDataBase() qDebug() << " element_info_table query : " << query_.lastError(); } + //Create the element_information table: the information of every + //symbol placed in the project, in full -- each key with its value + //and whether it is shown -- the store symbols write through + //(storeElementInformation()). Unlike element_info, which has one + //column per known key and is filled again on every rebuild, this + //is kept by the edits themselves. + if (!query_.exec(QStringLiteral( + "CREATE TABLE element_information (" + "element_uuid VARCHAR(50) NOT NULL, " + "ord INTEGER NOT NULL, " + "name TEXT NOT NULL, " + "value TEXT, " + "show INTEGER NOT NULL, " + "PRIMARY KEY (element_uuid, ord))"))) { + qDebug() << " element_information_table query : " << query_.lastError(); + } + //Create the folio_titleblock table: the title block properties of + //every folio, as a folio saves them, the store folios write + //through (storeFolioTitleBlock()). One row per property, then one + //per additional field ("custom:" and its name). + if (!query_.exec(QStringLiteral( + "CREATE TABLE folio_titleblock (" + "diagram_uuid VARCHAR(50) NOT NULL, " + "ord INTEGER NOT NULL, " + "name TEXT NOT NULL, " + "value TEXT, " + "show INTEGER NOT NULL, " + "PRIMARY KEY (diagram_uuid, ord))"))) { + qDebug() << " folio_titleblock_table query : " << query_.lastError(); + } + + //Create the conductor_properties table: the properties of every + //wire placed in the project, as the attributes a saved wire carries, + //the store wires write through (storeConductorProperties()). + if (!query_.exec(QStringLiteral( + "CREATE TABLE conductor_properties (" + "conductor_uuid VARCHAR(50) NOT NULL, " + "name TEXT NOT NULL, " + "value TEXT, " + "PRIMARY KEY (conductor_uuid, name))"))) { + qDebug() << " conductor_properties_table query : " << query_.lastError(); + } + + //Create the link table: one row per element and element it is //linked to -- a coil and its contacts, a pair of folio reports -- //from each side, with the contact group the element saved for it. @@ -2114,7 +2378,8 @@ void projectDataBase::populateDiagramTable() QSqlQuery query_(m_data_base); query_.exec("DELETE FROM diagram"); - for (auto diagram : m_project->diagrams()) + //The folios' own data: folios() builds nothing + for (auto diagram : m_project->folios()) { m_insert_diagram_query.bindValue(":uuid", diagram->uuid().toString()); m_insert_diagram_query.bindValue(":pos", m_project->folioIndex(diagram)+1); @@ -2158,8 +2423,12 @@ void projectDataBase::populateElementTable() QSqlQuery query_(m_data_base); query_.exec("DELETE FROM element"); - for (auto diagram : m_project->diagrams()) + for (auto diagram : m_project->folios()) { + if (m_kept_folios.contains(diagram)) { + restoreKeptRows(QStringLiteral("element"), diagram, QStringLiteral("diagram_uuid = :folio")); + continue; + } const ElementProvider ep(diagram); const auto elmt_vector = ep.find(allElementTypes()); //Insert all values into the database @@ -2175,20 +2444,73 @@ void projectDataBase::populateElementTable() /** @brief projectDataBase::populateElementInfoTable - Populate the element info table + Populate the element info table from the symbol information store + (DB-ACCESSORS-PLAN.md stage 4.3), in the order of the element table. + The label column is the label a symbol shows, which its folio's + numbering decides: the symbol holding the uuid gives it. + + A project where several symbols share a uuid (F100) is read from its + folios as before: the first symbol on them owns the row, and the store + does not know which one that is. */ void projectDataBase::populateElementInfoTable() { QSqlQuery query(m_data_base); query.exec(QStringLiteral("DELETE FROM element_info")); - for (const auto &diagram : m_project->diagrams()) - { - const ElementProvider ep(diagram); - const auto elmt_vector = ep.find(allElementTypes()); + for (auto it = m_placed_elements.constBegin() ; it != m_placed_elements.constEnd() ; ++it) { + if (placedElementCount(it.key()) > 1) { + populateElementInfoTableFromFolios(); + return; + } + } - //Insert all values into the database - for (const auto &elmt : elmt_vector) + QSqlQuery uuids(m_data_base); + if (!uuids.exec(QStringLiteral("SELECT uuid FROM element ORDER BY rowid"))) { + qDebug() << "projectDataBase::populateElementInfoTable select error : " << uuids.lastError(); + return; + } + while (uuids.next()) + { + const QString uuid_str = uuids.value(0).toString(); + const QUuid uuid(uuid_str); + Element *holder = otherHolder(uuid, nullptr); + if (!holder) { + //A symbol on a folio not built: its row as it was + if (!m_kept_folios.isEmpty()) { + QSqlQuery kept(m_data_base); + kept.prepare(QStringLiteral("INSERT INTO element_info SELECT * FROM kept_element_info " + "WHERE element_uuid = :uuid")); + kept.bindValue(QStringLiteral(":uuid"), uuid_str); + kept.exec(); + } + continue; + } + bindElementInfoValues(m_insert_element_info_query, uuid_str, + m_element_information.value(uuid), holder->actualLabel()); + if (!m_insert_element_info_query.exec()) { + qDebug() << "projectDataBase::populateElementInfoTable insert error : " << m_insert_element_info_query.lastError(); + } + } +} + +/** + @brief projectDataBase::populateElementInfoTableFromFolios + populateElementInfoTable() from the symbols on the folios +*/ +void projectDataBase::populateElementInfoTableFromFolios() +{ + for (const auto &diagram : m_project->folios()) + { + //A folio not built: its rows as they were + if (m_kept_folios.contains(diagram)) { + restoreKeptRows(QStringLiteral("element_info"), diagram, + QStringLiteral("element_uuid IN (SELECT uuid FROM kept_element " + "WHERE diagram_uuid = :folio)")); + continue; + } + const ElementProvider ep(diagram); + for (const auto &elmt : ep.find(allElementTypes())) { bindElementInfoValues(m_insert_element_info_query, elmt); if (!m_insert_element_info_query.exec()) { @@ -2198,6 +2520,798 @@ void projectDataBase::populateElementInfoTable() } } +/** + @brief projectDataBase::elementInformation + @return the stored information of the placed symbol @p element, empty + if it has none +*/ +DiagramContext projectDataBase::elementInformation(const QUuid &element) const +{ + return m_element_information.value(element); +} + +bool projectDataBase::hasElementInformation(const QUuid &element) const +{ + return m_element_information.contains(element); +} + +int projectDataBase::placedElementCount(const QUuid &element) const +{ + //Without values(), which makes a list: this is asked on every read + //of a placed symbol's information. + int n = 0; + for (auto it = m_placed_elements.constFind(element) ; + it != m_placed_elements.constEnd() && it.key() == element ; ++it) + if (it.value()) ++n; + return n; +} + +/** + @brief projectDataBase::storeElementInformation + Store @p information as the information of the placed symbol @p element, + in the element_information table and the cache in front of it. + @return true if that changed what was stored +*/ +bool projectDataBase::storeElementInformation(const QUuid &element, const DiagramContext &information) +{ + if (element.isNull()) return false; + auto known = m_element_information.constFind(element); + if (known != m_element_information.constEnd() && *known == information) return false; + m_element_information.insert(element, information); + + const QString uuid = element.toString(); + m_store_remove_query.bindValue(QStringLiteral(":uuid"), uuid); + if (!m_store_remove_query.exec()) { + qDebug() << "projectDataBase::storeElementInformation remove error : " << m_store_remove_query.lastError(); + } + QSqlQuery &insert = m_store_insert_query; + int ord = 0; + for (const QString &key : information.keys()) { + insert.bindValue(QStringLiteral(":uuid"), uuid); + insert.bindValue(QStringLiteral(":ord"), ord++); + insert.bindValue(QStringLiteral(":name"), key); + insert.bindValue(QStringLiteral(":value"), information.value(key).toString()); + insert.bindValue(QStringLiteral(":show"), information.keyMustShow(key) ? 1 : 0); + if (!insert.exec()) { + qDebug() << "projectDataBase::storeElementInformation insert error : " << insert.lastError(); + } + } + return true; +} + +/** + @brief projectDataBase::elementInformationChanged + The placed symbol @p element now has @p information: the store keeps + it, and its element_info row follows (F106: a pasted symbol's label is + erased or numbered after its row is written, by + Element::setElementInformations(), which tells no table). + A uuid other symbols share (F100) keeps the row of the first of them. +*/ +void projectDataBase::elementInformationChanged(Element *element, const DiagramContext &information) +{ + if (!storeElementInformation(element->uuid(), information)) { + return; + } + m_content_changed = true; + if (m_update_blocked || placedElementCount(element->uuid()) != 1) { + return; + } + const auto hash = elementInfoToString(element); + for (const auto &key : QETInformation::elementInfoKeys()) { + m_update_element_query.bindValue(QStringLiteral(":") + key, hash.value(key)); + } + m_update_element_query.bindValue(QStringLiteral(":uuid"), element->uuid().toString()); + if (!m_update_element_query.exec()) { + qDebug() << "projectDataBase::elementInformationChanged update error : " << m_update_element_query.lastError(); + } +} + +/** + @brief projectDataBase::forgetElementInformation + The symbol @p element is no longer placed: its stored information goes. +*/ +void projectDataBase::forgetElementInformation(const QUuid &element) +{ + if (!m_element_information.remove(element)) return; + m_store_remove_query.bindValue(QStringLiteral(":uuid"), element.toString()); + if (!m_store_remove_query.exec()) { + qDebug() << "projectDataBase::forgetElementInformation error : " << m_store_remove_query.lastError(); + } +} + +/** + @brief projectDataBase::placeElement + @p element is now placed: its information is stored under its uuid. +*/ +void projectDataBase::placeElement(Element *element) +{ + if (!m_placed_elements.contains(element->uuid(), element)) + m_placed_elements.insert(element->uuid(), element); + storeElementInformation(element->uuid(), element->ownInformations()); +} + +/** + @brief projectDataBase::unplaceElement + @p element no longer holds @p uuid. If another placed symbol still does + (the original of a copy), the row is written again from it; otherwise + it goes. +*/ +void projectDataBase::unplaceElement(Element *element, const QUuid &uuid) +{ + m_placed_elements.remove(uuid, element); + for (const QPointer &other : m_placed_elements.values(uuid)) { + if (other) { + //Its own copy: the store still holds this symbol's + storeElementInformation(uuid, other->ownInformations()); + return; + } + } + m_placed_elements.remove(uuid); + forgetElementInformation(uuid); +} + +/** + @brief projectDataBase::elementUuidChanged + The uuid of the placed symbol @p element changed from @p old_uuid (a + paste or a folio copy renews it): its information and its element, + element_info and link rows follow it. A copy is placed while it still + carries its original's uuid, so its rows could not be written then + (the uuid was taken): they are written now. The old uuid keeps its rows + only if another symbol still holds it (F106). +*/ +void projectDataBase::elementUuidChanged(Element *element, const QUuid &old_uuid) +{ + if (!m_placed_elements.contains(old_uuid, element) || old_uuid == element->uuid()) return; + m_content_changed = true; + Element *other = otherHolder(old_uuid, element); + unplaceElement(element, old_uuid); + removeElementRows(old_uuid); + if (other) { + //The old uuid's rows may have been written from this symbol + //while both held it: write them again from the one keeping it. + writeElementRows(other); + queueLinks(other); + for (Element *linked : other->linkedElements()) + queueLinks(linked); + } + placeElement(element); + writeElementRows(element); + //Its wires' rows name their ends' symbols: write them again + //(a pasted wire is placed before its symbols get their uuids). + for (Conductor *conductor : element->conductors()) { + if (placedConductorCount(conductor->uuid()) == 1) { + removeConductorRow(conductor->uuid()); + writeConductorRow(conductor); + } + } + queueLinks(element); + for (Element *linked : element->linkedElements()) + queueLinks(linked); + emit dataBaseUpdated(); +} + +/** + @brief projectDataBase::otherHolder + @return a placed symbol other than @p element holding @p uuid, or + nullptr. Two hold one uuid while a copy is being placed, and for good + in files several symbols share a uuid in (F100). +*/ +Element *projectDataBase::otherHolder(const QUuid &uuid, const Element *element) const +{ + for (const QPointer &other : m_placed_elements.values(uuid)) + if (other && other != element) return other; + return nullptr; +} + +/** + @brief projectDataBase::writeElementRows + Write the element and element_info rows of @p element, a placed symbol. + A uuid that already has rows keeps them: the first symbol to hold a + uuid owns its rows, as in a full rebuild. + + Nothing is written while updates are blocked (a project being opened, + a paste being read): the rebuild that ends them writes every row. +*/ +void projectDataBase::writeElementRows(Element *element) +{ + if (m_update_blocked) { + return; + } + bindElementValues(m_insert_elements_query, element, element->diagram()); + if (!m_insert_elements_query.exec()) { + qDebug() << "projectDataBase::writeElementRows insert element error : " << m_insert_elements_query.lastError(); + } + bindElementInfoValues(m_insert_element_info_query, element); + if (!m_insert_element_info_query.exec()) { + qDebug() << "projectDataBase::writeElementRows insert element info error : " << m_insert_element_info_query.lastError(); + } +} + +/** + @brief projectDataBase::removeElementRows + Remove the element, element_info and link rows of @p uuid; nothing + while updates are blocked, as writeElementRows(). +*/ +void projectDataBase::removeElementRows(const QUuid &uuid) +{ + if (m_update_blocked) { + return; + } + const QString uuid_str = uuid.toString(); + m_remove_element_query.bindValue(QStringLiteral(":uuid"), uuid_str); + if (!m_remove_element_query.exec()) { + qDebug() << "projectDataBase::removeElementRows remove error : " << m_remove_element_query.lastError(); + } + m_remove_element_info_query.bindValue(QStringLiteral(":uuid"), uuid_str); + if (!m_remove_element_info_query.exec()) { + qDebug() << "projectDataBase::removeElementRows remove element_info error : " << m_remove_element_info_query.lastError(); + } + m_remove_links_query.bindValue(QStringLiteral(":uuid"), uuid_str); + if (!m_remove_links_query.exec()) { + qDebug() << "projectDataBase::removeElementRows remove link error : " << m_remove_links_query.lastError(); + } +} + +/** + @brief projectDataBase::elementInformationMismatches + Compare the store with every placed symbol, both ways: a symbol whose + information differs or is missing, and a stored row no symbol has. +*/ +QStringList projectDataBase::elementInformationMismatches() const +{ + QStringList out; + QSet placed; + for (Diagram *diagram : m_project->diagrams()) { + for (Element *element : diagram->elements()) { + placed.insert(element->uuid()); + //A uuid several symbols share has one row and cannot agree + //with them all; those symbols answer from their own copy. + if (placedElementCount(element->uuid()) > 1) + continue; + if (!m_element_information.contains(element->uuid())) + out << element->uuid().toString() + QStringLiteral(": not stored"); + else if (m_element_information.value(element->uuid()) != element->elementInformations()) + out << element->uuid().toString() + QStringLiteral(": differs"); + } + } + for (auto it = m_element_information.constBegin() ; it != m_element_information.constEnd() ; ++it) + if (!placed.contains(it.key())) + out << it.key().toString() + QStringLiteral(": stored, not placed"); + QSqlQuery rows(m_data_base); + if (rows.exec(QStringLiteral("SELECT count(*) FROM element_information")) && rows.next()) { + int expected = 0; + for (const DiagramContext &dc : m_element_information) expected += int(dc.keys().size()); + if (rows.value(0).toInt() != expected) + out << QStringLiteral("table holds %1 rows, the cache %2").arg(rows.value(0).toInt()).arg(expected); + } + return out; +} + +/** + @brief projectDataBase::folioTitleBlock + @return the stored title block properties of the folio @p folio +*/ +TitleBlockProperties projectDataBase::folioTitleBlock(const QUuid &folio) const +{ + return m_folio_titleblocks.value(folio); +} + +/** + @brief projectDataBase::storedFolioTitleBlock + @return the stored title block properties of the folio @p folio, or + nullptr; valid until the store next changes +*/ +const TitleBlockProperties *projectDataBase::storedFolioTitleBlock(const QUuid &folio) const +{ + auto it = m_folio_titleblocks.constFind(folio); + return it == m_folio_titleblocks.constEnd() ? nullptr : &it.value(); +} + +/** + @brief projectDataBase::placedFolio + @return the folio of the project holding @p folio, or nullptr +*/ +Diagram *projectDataBase::placedFolio(const QUuid &folio) const +{ + return m_placed_folios.value(folio); +} + +/** + @brief projectDataBase::storeFolioTitleBlock + Store @p properties as the title block of the folio @p folio, in the + folio_titleblock table and the cache in front of it. +*/ +void projectDataBase::storeFolioTitleBlock(const QUuid &folio, const TitleBlockProperties &properties) +{ + if (folio.isNull()) return; + auto known = m_folio_titleblocks.find(folio); + if (known != m_folio_titleblocks.end() && *known == properties) return; + m_folio_titleblocks.insert(folio, properties); + + const QString uuid = folio.toString(); + QSqlQuery remove(m_data_base); + remove.prepare(QStringLiteral("DELETE FROM folio_titleblock WHERE diagram_uuid = :uuid")); + remove.bindValue(QStringLiteral(":uuid"), uuid); + if (!remove.exec()) { + qDebug() << "projectDataBase::storeFolioTitleBlock remove error : " << remove.lastError(); + } + QSqlQuery insert(m_data_base); + insert.prepare(QStringLiteral("INSERT INTO folio_titleblock (diagram_uuid, ord, name, value, show) " + "VALUES (:uuid, :ord, :name, :value, :show)")); + int ord = 0; + auto row = [&](const QString &name, const QString &value, bool show) { + insert.bindValue(QStringLiteral(":uuid"), uuid); + insert.bindValue(QStringLiteral(":ord"), ord++); + insert.bindValue(QStringLiteral(":name"), name); + insert.bindValue(QStringLiteral(":value"), value); + insert.bindValue(QStringLiteral(":show"), show ? 1 : 0); + if (!insert.exec()) { + qDebug() << "projectDataBase::storeFolioTitleBlock insert error : " << insert.lastError(); + } + }; + row(QStringLiteral("title"), properties.title, true); + row(QStringLiteral("author"), properties.author, true); + row(QStringLiteral("date"), properties.date.isValid() + ? properties.date.toString(Qt::ISODate) : QString(), true); + row(QStringLiteral("filename"), properties.filename, true); + row(QStringLiteral("plant"), properties.plant, true); + row(QStringLiteral("locmach"), properties.locmach, true); + row(QStringLiteral("indexrev"), properties.indexrev, true); + row(QStringLiteral("version"), properties.version, true); + row(QStringLiteral("folio"), properties.folio, true); + row(QStringLiteral("auto_page_num"), properties.auto_page_num, true); + row(QStringLiteral("template_name"), properties.template_name, true); + row(QStringLiteral("display_at"), properties.display_at == Qt::RightEdge + ? QStringLiteral("right") : QStringLiteral("bottom"), true); + for (const QString &key : properties.context.keys()) { + row(QStringLiteral("custom:") + key, properties.context.value(key).toString(), + properties.context.keyMustShow(key)); + } +} + +/** + @brief projectDataBase::placeFolio + @p folio is now a folio of the project: its title block is stored under + its uuid. +*/ +void projectDataBase::placeFolio(Diagram *folio) +{ + m_placed_folios.insert(folio->uuid(), folio); + storeFolioTitleBlock(folio->uuid(), folio->border_and_titleblock.localTitleBlock()); +} + +/** + @brief projectDataBase::unplaceFolio + @p folio no longer holds @p uuid (it was removed, or its uuid changed): + the title block stored under it goes. +*/ +void projectDataBase::unplaceFolio(Diagram *folio, const QUuid &uuid) +{ + if (m_placed_folios.value(uuid) != folio) return; + m_placed_folios.remove(uuid); + m_folio_titleblocks.remove(uuid); + QSqlQuery remove(m_data_base); + remove.prepare(QStringLiteral("DELETE FROM folio_titleblock WHERE diagram_uuid = :uuid")); + remove.bindValue(QStringLiteral(":uuid"), uuid.toString()); + if (!remove.exec()) { + qDebug() << "projectDataBase::unplaceFolio error : " << remove.lastError(); + } +} + +/** + @brief projectDataBase::folioUuidChanged + The uuid of the placed folio @p folio changed from @p old_uuid: its + stored title block follows it. +*/ +void projectDataBase::folioUuidChanged(Diagram *folio, const QUuid &old_uuid) +{ + if (old_uuid == folio->uuid() || m_placed_folios.value(old_uuid) != folio) return; + unplaceFolio(folio, old_uuid); + placeFolio(folio); +} + +/** + @brief projectDataBase::folioTitleBlockMismatches + Compare the store with every folio's own title block, both ways. +*/ +QStringList projectDataBase::folioTitleBlockMismatches() const +{ + QStringList out; + QSet folios; + for (Diagram *diagram : m_project->diagrams()) { + folios.insert(diagram->uuid()); + if (m_placed_folios.value(diagram->uuid()) != diagram) + out << diagram->uuid().toString() + QStringLiteral(": not stored"); + else if (TitleBlockProperties(m_folio_titleblocks.value(diagram->uuid())) + != diagram->border_and_titleblock.localTitleBlock()) + out << diagram->uuid().toString() + QStringLiteral(": differs"); + } + for (auto it = m_folio_titleblocks.constBegin() ; it != m_folio_titleblocks.constEnd() ; ++it) + if (!folios.contains(it.key())) + out << it.key().toString() + QStringLiteral(": stored, not a folio"); + return out; +} + +/** + @brief projectDataBase::conductorProperties + @return the stored properties of the placed wire @p conductor +*/ +ConductorProperties projectDataBase::conductorProperties(const QUuid &conductor) const +{ + return m_conductor_properties.value(conductor); +} + +bool projectDataBase::hasConductorProperties(const QUuid &conductor) const +{ + return m_conductor_properties.contains(conductor); +} + +int projectDataBase::placedConductorCount(const QUuid &conductor) const +{ + //Without values(), as placedElementCount() + int n = 0; + for (auto it = m_placed_conductors.constFind(conductor) ; + it != m_placed_conductors.constEnd() && it.key() == conductor ; ++it) + if (it.value()) ++n; + return n; +} + +/** + @brief projectDataBase::storeConductorProperties + Store @p properties as the properties of the placed wire @p conductor, + in the conductor_properties table -- the attributes a saved wire + carries -- and the cache in front of it. + @return true if that changed what was stored +*/ +bool projectDataBase::storeConductorProperties(const QUuid &conductor, const ConductorProperties &properties) +{ + if (conductor.isNull()) return false; + auto known = m_conductor_properties.constFind(conductor); + if (known != m_conductor_properties.constEnd() && *known == properties) return false; + m_conductor_properties.insert(conductor, properties); + //The rows are written by the next flushConductorProperties(): a + //folio inserted or removed renumbers the wires of every folio after + //it, one change each, and nothing reads them in between. + m_dirty_conductor_properties.insert(conductor); + return true; +} + +/** + @brief projectDataBase::flushConductorProperties + Write the conductor_properties rows of every wire changed since the last + flush; only the attributes that changed. +*/ +void projectDataBase::flushConductorProperties() +{ + if (m_dirty_conductor_properties.isEmpty()) { + return; + } + const QSet dirty = m_dirty_conductor_properties; + m_dirty_conductor_properties.clear(); + const bool own_transaction = m_data_base.transaction(); + for (const QUuid &conductor : dirty) + { + const QString uuid = conductor.toString(); + QHash attributes; + auto known = m_conductor_properties.constFind(conductor); + if (known != m_conductor_properties.constEnd()) { + for (const auto &attribute : known->attributes()) + attributes.insert(attribute.first, attribute.second); + } + QHash &rows = m_conductor_attributes[conductor]; + for (auto it = rows.constBegin() ; it != rows.constEnd() ; ++it) { + if (attributes.contains(it.key())) continue; + m_conductor_property_remove_query.bindValue(QStringLiteral(":uuid"), uuid); + m_conductor_property_remove_query.bindValue(QStringLiteral(":name"), it.key()); + if (!m_conductor_property_remove_query.exec()) { + qDebug() << "projectDataBase::flushConductorProperties remove error : " << m_conductor_property_remove_query.lastError(); + } + } + for (auto it = attributes.constBegin() ; it != attributes.constEnd() ; ++it) { + auto row = rows.constFind(it.key()); + if (row != rows.constEnd() && *row == it.value()) continue; + m_conductor_properties_insert_query.bindValue(QStringLiteral(":uuid"), uuid); + m_conductor_properties_insert_query.bindValue(QStringLiteral(":name"), it.key()); + m_conductor_properties_insert_query.bindValue(QStringLiteral(":value"), it.value()); + if (!m_conductor_properties_insert_query.exec()) { + qDebug() << "projectDataBase::flushConductorProperties insert error : " << m_conductor_properties_insert_query.lastError(); + } + } + if (attributes.isEmpty()) + m_conductor_attributes.remove(conductor); + else + rows = attributes; + + //The conductor row's text, if the row is this wire's alone + if (known != m_conductor_properties.constEnd() && !m_update_blocked + && placedConductorCount(conductor) == 1) { + m_update_conductor_query.bindValue(QStringLiteral(":uuid"), uuid); + m_update_conductor_query.bindValue(QStringLiteral(":text"), known->text); + if (!m_update_conductor_query.exec()) { + qDebug() << "projectDataBase::flushConductorProperties update error : " << m_update_conductor_query.lastError(); + } + } + } + if (own_transaction) { + m_data_base.commit(); + } +} + +/** + @brief projectDataBase::conductorPropertiesStored + The placed wire @p conductor now has @p properties: the store keeps + them, and its conductor row's text follows at the next flush (a formula + is numbered by Conductor::refreshText(), which tells no table). +*/ +void projectDataBase::conductorPropertiesStored(Conductor *conductor, const ConductorProperties &properties) +{ + if (storeConductorProperties(conductor->uuid(), properties)) { + m_content_changed = true; + } +} + +/** + @brief projectDataBase::placeConductor + @p conductor is now placed: its properties are stored under its uuid. +*/ +void projectDataBase::placeConductor(Conductor *conductor) +{ + if (!m_placed_conductors.contains(conductor->uuid(), conductor)) + m_placed_conductors.insert(conductor->uuid(), conductor); + storeConductorProperties(conductor->uuid(), conductor->ownProperties()); +} + +/** + @brief projectDataBase::unplaceConductor + @p conductor no longer holds @p uuid. If another placed wire still does + (the original of a copy), its properties are stored again; otherwise + they go. +*/ +void projectDataBase::unplaceConductor(Conductor *conductor, const QUuid &uuid) +{ + m_placed_conductors.remove(uuid, conductor); + if (Conductor *other = otherConductorHolder(uuid, nullptr)) { + storeConductorProperties(uuid, other->ownProperties()); + return; + } + m_placed_conductors.remove(uuid); + if (m_conductor_properties.remove(uuid)) + m_dirty_conductor_properties.insert(uuid); +} + +/** + @brief projectDataBase::otherConductorHolder + @return a placed wire other than @p conductor holding @p uuid, or nullptr +*/ +Conductor *projectDataBase::otherConductorHolder(const QUuid &uuid, const Conductor *conductor) const +{ + for (const QPointer &other : m_placed_conductors.values(uuid)) + if (other && other != conductor) return other; + return nullptr; +} + +/** + @brief projectDataBase::conductorUuidChanged + The uuid of the placed wire @p conductor changed from @p old_uuid (a + paste renews it, a file gives it): its properties and its row follow + it. A copy is placed while it still carries its original's uuid, so its + row could not be written then; the original's is written again. +*/ +void projectDataBase::conductorUuidChanged(Conductor *conductor, const QUuid &old_uuid) +{ + if (!m_placed_conductors.contains(old_uuid, conductor) || old_uuid == conductor->uuid()) return; + m_content_changed = true; + Conductor *other = otherConductorHolder(old_uuid, conductor); + unplaceConductor(conductor, old_uuid); + removeConductorRow(old_uuid); + if (other) { + writeConductorRow(other); + } + placeConductor(conductor); + writeConductorRow(conductor); + emit dataBaseUpdated(); +} + +/** + @brief projectDataBase::conductorPropertiesMismatches + Compare the store with every placed wire, both ways. +*/ +QStringList projectDataBase::conductorPropertiesMismatches() const +{ + QStringList out; + QSet placed; + for (Diagram *diagram : m_project->diagrams()) { + for (Conductor *conductor : diagram->conductors()) { + placed.insert(conductor->uuid()); + if (placedConductorCount(conductor->uuid()) > 1) + continue; + if (!m_conductor_properties.contains(conductor->uuid())) + out << conductor->uuid().toString() + QStringLiteral(": not stored"); + else if (m_conductor_properties.value(conductor->uuid()) != conductor->ownProperties()) + out << conductor->uuid().toString() + QStringLiteral(": differs"); + } + } + for (auto it = m_conductor_properties.constBegin() ; it != m_conductor_properties.constEnd() ; ++it) + if (!placed.contains(it.key())) + out << it.key().toString() + QStringLiteral(": stored, not placed"); + return out; +} + +/** + @brief projectDataBase::prefillFromDocument + Fill the stores of symbol information, folio title blocks and wire + properties from @p document, the project being opened, before its + folios are built: what step 5 (lazy folios) needs, a folio's data + without its scene. In a .qetz these blocks come from project.sqlite. + + Only what the document names by a uuid it holds once: a symbol, wire + or folio without one gets its uuid while it is built, and a uuid + several symbols share (F100) names none of them. Building then writes + the same stores again; endPrefill() says how often it agreed. +*/ +void projectDataBase::prefillFromDocument(const QDomDocument &document) +{ + m_prefilled_elements.clear(); + m_prefilled_folios.clear(); + m_prefilled_conductors.clear(); + for (PrefillTally &tally : m_prefill_tally) tally = PrefillTally(); + + QHash seen; + QHash elements; + QHash conductors; + const QDomElement root = document.documentElement(); + for (QDomElement folio = root.firstChildElement(QStringLiteral("diagram")) ; !folio.isNull() ; + folio = folio.nextSiblingElement(QStringLiteral("diagram"))) + { + const QUuid folio_uuid(folio.attribute(QStringLiteral("uuid"))); + if (!folio_uuid.isNull() && ++seen[folio_uuid] == 1) { + TitleBlockProperties properties; + properties.fromXml(folio); + //What BorderTitleBlock::importTitleBlock() makes of them + properties.version = QetVersion::displayedVersion(); + properties.collection = QET::QetCollection::Embedded; + m_prefilled_folios.insert(folio_uuid, properties); + } + const QDomElement element_list = folio.firstChildElement(QStringLiteral("elements")); + for (QDomElement element = element_list.firstChildElement(QStringLiteral("element")) ; + !element.isNull() ; element = element.nextSiblingElement(QStringLiteral("element"))) + { + const QUuid uuid(element.attribute(QStringLiteral("uuid"))); + if (uuid.isNull() || ++seen[uuid] > 1) continue; + DiagramContext information; + information.fromXml(element.firstChildElement(QStringLiteral("elementInformations")), + QStringLiteral("elementInformation")); + elements.insert(uuid, information); + } + const QDomElement conductor_list = folio.firstChildElement(QStringLiteral("conductors")); + for (QDomElement conductor = conductor_list.firstChildElement(QStringLiteral("conductor")) ; + !conductor.isNull() ; conductor = conductor.nextSiblingElement(QStringLiteral("conductor"))) + { + const QUuid uuid(conductor.attribute(QStringLiteral("uuid"))); + if (uuid.isNull() || ++seen[uuid] > 1) continue; + ConductorProperties properties; + properties.fromXml(conductor); + conductors.insert(uuid, properties); + } + } + //A uuid held twice names nothing + for (auto it = elements.constBegin() ; it != elements.constEnd() ; ++it) + if (seen.value(it.key()) == 1) m_prefilled_elements.insert(it.key(), it.value()); + for (auto it = conductors.constBegin() ; it != conductors.constEnd() ; ++it) + if (seen.value(it.key()) == 1) m_prefilled_conductors.insert(it.key(), it.value()); + for (auto it = m_prefilled_folios.begin() ; it != m_prefilled_folios.end() ; ) + it = seen.value(it.key()) == 1 ? std::next(it) : m_prefilled_folios.erase(it); + + for (auto it = m_prefilled_elements.constBegin() ; it != m_prefilled_elements.constEnd() ; ++it) + storeElementInformation(it.key(), it.value()); + for (auto it = m_prefilled_folios.constBegin() ; it != m_prefilled_folios.constEnd() ; ++it) + storeFolioTitleBlock(it.key(), it.value()); + for (auto it = m_prefilled_conductors.constBegin() ; it != m_prefilled_conductors.constEnd() ; ++it) + storeConductorProperties(it.key(), it.value()); +} + +/** + @brief projectDataBase::endPrefill + The folios are built: compare what prefillFromDocument() stored with + what building stored, and forget what nothing was placed for -- unless + @p keep_unplaced, when some folios are not built yet (QET_LAZY_FOLIOS): + the store then answers for their items until they are. +*/ +void projectDataBase::endPrefill(bool keep_unplaced) +{ + auto tally = [](PrefillTally &t, const QStringList &differing) { + if (differing.isEmpty()) { ++t.agree; return; } + ++t.differ; + for (const QString &field : differing) ++t.fields[field]; + }; + for (auto it = m_prefilled_elements.constBegin() ; it != m_prefilled_elements.constEnd() ; ++it) { + if (!placedElementCount(it.key())) { + ++m_prefill_tally[0].unplaced; + if (!keep_unplaced && !m_placed_elements.contains(it.key())) forgetElementInformation(it.key()); + continue; + } + const DiagramContext built = m_element_information.value(it.key()); + QStringList differing; + const QList built_keys = built.keys(); + QSet names(built_keys.cbegin(), built_keys.cend()); + const QList prefilled_keys = it.value().keys(); + names.unite(QSet(prefilled_keys.cbegin(), prefilled_keys.cend())); + for (const QString &name : names) + if (built.value(name) != it.value().value(name) + || built.keyMustShow(name) != it.value().keyMustShow(name)) + differing << name; + tally(m_prefill_tally[0], differing); + } + for (auto it = m_prefilled_folios.constBegin() ; it != m_prefilled_folios.constEnd() ; ++it) { + if (!m_placed_folios.value(it.key())) { + ++m_prefill_tally[1].unplaced; + if (!keep_unplaced) m_folio_titleblocks.remove(it.key()); + continue; + } + const TitleBlockProperties built = m_folio_titleblocks.value(it.key()); + const TitleBlockProperties &p = it.value(); + QStringList differing; + if (built.title != p.title) differing << QStringLiteral("title"); + if (built.author != p.author) differing << QStringLiteral("author"); + if (built.date != p.date) differing << QStringLiteral("date"); + if (built.filename != p.filename) differing << QStringLiteral("filename"); + if (built.plant != p.plant) differing << QStringLiteral("plant"); + if (built.locmach != p.locmach) differing << QStringLiteral("locmach"); + if (built.indexrev != p.indexrev) differing << QStringLiteral("indexrev"); + if (built.version != p.version) differing << QStringLiteral("version"); + if (built.folio != p.folio) differing << QStringLiteral("folio"); + if (built.auto_page_num != p.auto_page_num) differing << QStringLiteral("auto_page_num"); + if (built.template_name != p.template_name) differing << QStringLiteral("template_name"); + if (!(built.context == p.context)) differing << QStringLiteral("context"); + if (built.display_at != p.display_at) differing << QStringLiteral("display_at"); + if (built.collection != p.collection) differing << QStringLiteral("collection"); + tally(m_prefill_tally[1], differing); + } + for (auto it = m_prefilled_conductors.constBegin() ; it != m_prefilled_conductors.constEnd() ; ++it) { + if (!placedConductorCount(it.key())) { + ++m_prefill_tally[2].unplaced; + if (!keep_unplaced && !m_placed_conductors.contains(it.key()) && m_conductor_properties.remove(it.key())) + m_dirty_conductor_properties.insert(it.key()); + continue; + } + QHash built, prefilled; + for (const auto &a : m_conductor_properties.value(it.key()).attributes()) built.insert(a.first, a.second); + for (const auto &a : it.value().attributes()) prefilled.insert(a.first, a.second); + QStringList differing; + QSet names(built.keyBegin(), built.keyEnd()); + names.unite(QSet(prefilled.keyBegin(), prefilled.keyEnd())); + for (const QString &name : names) + if (built.value(name) != prefilled.value(name)) differing << name; + tally(m_prefill_tally[2], differing); + } + m_prefilled_elements.clear(); + m_prefilled_folios.clear(); + m_prefilled_conductors.clear(); +} + +/** + @brief projectDataBase::prefillReport + @return how the last prefillFromDocument() compared with building, one + line: per store, values that agreed, differed (and in which fields), + and were not placed +*/ +QString projectDataBase::prefillReport() const +{ + static const char *names[] = {"symbols", "folios", "wires"}; + QStringList parts; + for (int i = 0 ; i < 3 ; ++i) { + const PrefillTally &t = m_prefill_tally[i]; + QStringList fields; + for (auto it = t.fields.constBegin() ; it != t.fields.constEnd() ; ++it) + fields << QStringLiteral("%1 %2").arg(it.key()).arg(it.value()); + parts << QStringLiteral("%1 %2 agree, %3 differ%4, %5 not placed") + .arg(QLatin1String(names[i])).arg(t.agree).arg(t.differ) + .arg(fields.isEmpty() ? QString() : QStringLiteral(" (") + fields.join(QStringLiteral(", ")) + QLatin1Char(')')) + .arg(t.unplaced); + } + return parts.join(QStringLiteral("; ")); +} + /** @brief projectDataBase::populateLinkTable Populate the link table from the built folios @@ -2210,17 +3324,30 @@ void projectDataBase::populateLinkTable() query.prepare(QStringLiteral("INSERT INTO link (element_uuid, linked_uuid, group_index) " "VALUES (:element_uuid, :linked_uuid, :group_index)")); - for (const auto &diagram : m_project->diagrams()) + for (const auto &diagram : m_project->folios()) { + //A folio not built: its rows, but to a partner since removed + if (m_kept_folios.contains(diagram)) { + restoreKeptRows(QStringLiteral("link"), diagram, + QStringLiteral("element_uuid IN (SELECT uuid FROM kept_element " + "WHERE diagram_uuid = :folio) " + "AND linked_uuid IN (SELECT uuid FROM element)")); + continue; + } const ElementProvider ep(diagram); for (const auto &elmt : ep.find(allElementTypes())) { + QList> rows; for (Element *linked : elmt->linkedElements()) + rows.append({linked->uuid().toString(), elmt->groupIndexForElement(linked)}); + //Partners on a folio not built, which it will be linked to + for (const auto &waiting : elmt->waitingLinks()) + rows.append({waiting.first.toString(), waiting.second}); + for (const auto &row : std::as_const(rows)) { - const int group = elmt->groupIndexForElement(linked); query.bindValue(QStringLiteral(":element_uuid"), elmt->uuid().toString()); - query.bindValue(QStringLiteral(":linked_uuid"), linked->uuid().toString()); - query.bindValue(QStringLiteral(":group_index"), group >= 0 ? QVariant(group) : QVariant()); + query.bindValue(QStringLiteral(":linked_uuid"), row.first); + query.bindValue(QStringLiteral(":group_index"), row.second >= 0 ? QVariant(row.second) : QVariant()); if (!query.exec()) { qDebug() << "projectDataBase::populateLinkTable insert error : " << query.lastError(); } @@ -2235,9 +3362,17 @@ void projectDataBase::populateLinkTable() by the next flushLinks(). */ void projectDataBase::linksChanged() +{ + queueLinks(qobject_cast(sender())); +} + +/** + @brief projectDataBase::queueLinks + The link rows of @p element are written again by the next flushLinks(). +*/ +void projectDataBase::queueLinks(Element *element) { m_content_changed = true; - auto *element = qobject_cast(sender()); if (element && !m_dirty_link_elements.contains(element)) { m_dirty_link_elements << element; } @@ -2271,9 +3406,18 @@ void projectDataBase::flushLinks() qDebug() << "projectDataBase::flushLinks remove error : " << remove.lastError(); } //An element taken off its folio is unlinked first (Diagram:: - //removeItem()), so it writes no rows here. + //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)) { + continue; + } for (Element *linked : element->linkedElements()) { + //A symbol on a removed folio stays linked, for the undo, + //but the project no longer has it + if (!m_placed_elements.contains(linked->uuid(), linked)) { + continue; + } const int group = element->groupIndexForElement(linked); insert.bindValue(QStringLiteral(":element_uuid"), element->uuid().toString()); insert.bindValue(QStringLiteral(":linked_uuid"), linked->uuid().toString()); @@ -2282,6 +3426,17 @@ void projectDataBase::flushLinks() qDebug() << "projectDataBase::flushLinks insert error : " << insert.lastError(); } } + //Partners on a folio not built (QET_LAZY_FOLIOS), which it will + //be linked to once that folio is + for (const auto &waiting : element->waitingLinks()) + { + insert.bindValue(QStringLiteral(":element_uuid"), element->uuid().toString()); + insert.bindValue(QStringLiteral(":linked_uuid"), waiting.first.toString()); + insert.bindValue(QStringLiteral(":group_index"), waiting.second >= 0 ? QVariant(waiting.second) : QVariant()); + if (!insert.exec()) { + qDebug() << "projectDataBase::flushLinks insert error : " << insert.lastError(); + } + } } if (own_transaction) { m_data_base.commit(); @@ -2293,7 +3448,8 @@ void projectDataBase::populateDiagramInfoTable() QSqlQuery query(m_data_base); query.exec("DELETE FROM diagram_info"); - for (auto *diagram : m_project->diagrams()) + //The folios' own data: folios() builds nothing + for (auto *diagram : m_project->folios()) { bindDiagramInfoValues(m_insert_diagram_info_query, diagram); @@ -2315,8 +3471,15 @@ void projectDataBase::populateConductorTable() query.exec(QStringLiteral("DELETE FROM conductor")); query.exec(QStringLiteral("DELETE FROM terminal")); - for (auto *diagram : m_project->diagrams()) + for (auto *diagram : m_project->folios()) { + if (m_kept_folios.contains(diagram)) { + restoreKeptRows(QStringLiteral("terminal"), diagram, + QStringLiteral("element_uuid IN (SELECT uuid FROM kept_element " + "WHERE diagram_uuid = :folio)")); + restoreKeptRows(QStringLiteral("conductor"), diagram, QStringLiteral("diagram_uuid = :folio")); + continue; + } const auto conductor_list = diagram->conductors(); for (auto *conductor : conductor_list) { @@ -2401,6 +3564,20 @@ void projectDataBase::prepareQuery() m_cascade_remove_element_query.prepare( "DELETE FROM element WHERE diagram_uuid = :uuid"); + //The symbol information store and an element's link rows + m_store_remove_query = QSqlQuery(m_data_base); + m_store_remove_query.prepare(QStringLiteral("DELETE FROM element_information WHERE element_uuid = :uuid")); + m_store_insert_query = QSqlQuery(m_data_base); + m_store_insert_query.prepare(QStringLiteral("INSERT INTO element_information (element_uuid, ord, name, value, show) " + "VALUES (:uuid, :ord, :name, :value, :show)")); + m_conductor_properties_insert_query = QSqlQuery(m_data_base); + m_conductor_properties_insert_query.prepare(QStringLiteral("INSERT OR REPLACE INTO conductor_properties (conductor_uuid, name, value) " + "VALUES (:uuid, :name, :value)")); + m_conductor_property_remove_query = QSqlQuery(m_data_base); + m_conductor_property_remove_query.prepare(QStringLiteral("DELETE FROM conductor_properties WHERE conductor_uuid = :uuid AND name = :name")); + m_remove_links_query = QSqlQuery(m_data_base); + m_remove_links_query.prepare(QStringLiteral("DELETE FROM link WHERE element_uuid = :uuid OR linked_uuid = :uuid")); + m_remove_diagram_query = QSqlQuery(m_data_base); m_remove_diagram_query.prepare("DELETE FROM diagram WHERE uuid=:uuid"); @@ -2689,9 +3866,13 @@ void projectDataBase::exportDb(projectDataBase *db, // VACUUM INTO creates a standalone copy of the current database without // requiring access to the SQLite driver's native connection handle. const auto escaped_path = path_.replace("'", "''"); + if (db->m_project) { + db->m_project->loadFolios(); //the drawing tables need built folios + } db->flushDrawingItems(); db->flushLinks(); db->flushElementPositions(); + db->flushConductorProperties(); QSqlQuery query(db->m_data_base); if (!query.exec("VACUUM INTO '" % escaped_path % "'")) { qWarning() << "Unable to export project database:" << query.lastError().text(); diff --git a/sources/dataBase/projectdatabase.h b/sources/dataBase/projectdatabase.h index 9f3779c7c..0dd92aaa1 100644 --- a/sources/dataBase/projectdatabase.h +++ b/sources/dataBase/projectdatabase.h @@ -24,9 +24,15 @@ #include #include #include +#include +#include #include #include +#include "../diagramcontext.h" +#include "../titleblockproperties.h" +#include "../conductorproperties.h" + class Element; class QETProject; class Diagram; @@ -98,6 +104,62 @@ class projectDataBase : public QObject QSqlDatabase database() const {return m_data_base;} int excludedConductorCount() const; + //The information of every symbol placed in the project, by + //symbol uuid: the store each symbol's information is written + //through (DB-ACCESSORS-PLAN.md stage 4.1). A symbol not placed + //(a preview, the symbol editor) keeps its own. + DiagramContext elementInformation(const QUuid &element) const; + bool hasElementInformation(const QUuid &element) const; + //How many placed symbols hold @p element: more than one in a + //file whose symbols were copied by an old version (F100), or + //for a moment while a copy's uuid is renewed + int placedElementCount(const QUuid &element) const; + bool storeElementInformation(const QUuid &element, const DiagramContext &information); + void elementInformationChanged(Element *element, const DiagramContext &information); + void forgetElementInformation(const QUuid &element); + void elementUuidChanged(Element *element, const QUuid &old_uuid); + //Placed symbols whose information differs from the store's, + //each as "uuid: why"; empty when they all agree + QStringList elementInformationMismatches() const; + + //The title block properties of every folio of the project, by + //folio uuid: the store each folio's title block is written + //through (DB-ACCESSORS-PLAN.md stage 4.4). A folio not in a + //project keeps its own. + TitleBlockProperties folioTitleBlock(const QUuid &folio) const; + const TitleBlockProperties *storedFolioTitleBlock(const QUuid &folio) const; + Diagram *placedFolio(const QUuid &folio) const; + void storeFolioTitleBlock(const QUuid &folio, const TitleBlockProperties &properties); + void placeFolio(Diagram *folio); + void folioUuidChanged(Diagram *folio, const QUuid &old_uuid); + QStringList folioTitleBlockMismatches() const; + + //The properties of every wire placed in the project, by wire + //uuid: the store each wire's properties are written through + //(DB-ACCESSORS-PLAN.md stage 4.5). A wire not placed keeps its own. + ConductorProperties conductorProperties(const QUuid &conductor) const; + bool hasConductorProperties(const QUuid &conductor) const; + int placedConductorCount(const QUuid &conductor) const; + void conductorPropertiesStored(Conductor *conductor, const ConductorProperties &properties); + void conductorUuidChanged(Conductor *conductor, const QUuid &old_uuid); + QStringList conductorPropertiesMismatches() const; + + //Fill the three stores from a project's document before its + //folios are built (DB-ACCESSORS-PLAN.md stage 4.6), then, once + //they are, say how many of those values building agreed with. + void prefillFromDocument(const QDomDocument &document); + void endPrefill(bool keep_unplaced = false); + //A folio of a project opened with QET_LAZY_FOLIOS is being built: + //its rows came from the document already, so none are written + //and the database is not marked changed + void setBuildingFolio(bool building); + bool isBuildingFolio() const {return m_building_folios > 0;} + bool rowsFromDocument() const {return m_rows_from_document;} + QString prefillReport() const; + QSet foliosWithUnjoinedWires() const; + QSet folioUuidsOfElements(const QSet &elements) const; + QHash elementTypesOnFolios(const QSet &folios) const; + void addElement (Element *element); void removeElement (Element *element); void elementInfoChanged (Element *element); @@ -145,6 +207,7 @@ class projectDataBase : public QObject void flushElementPositions(); void populateElementTable(); void populateElementInfoTable(); + void populateElementInfoTableFromFolios(); void populateDiagramInfoTable(); void populateConductorTable(); void populateLinkTable(); @@ -203,7 +266,12 @@ class projectDataBase : public QObject m_cascade_remove_element_query, m_insert_shape_query, m_insert_independent_text_query, - m_insert_image_query; + m_insert_image_query, + m_store_remove_query, + m_store_insert_query, + m_remove_links_query, + m_conductor_properties_insert_query, + m_conductor_property_remove_query; //Which uuid's row each drawing item last wrote, and which item //wrote each row. A pasted copy is added to the folio still @@ -222,6 +290,51 @@ class projectDataBase : public QObject //sends xChanged and yChanged for every step of a drag, so the //cell is written when next read, as drawing items are. QList> m_moved_elements; + QHash m_element_information; + //The placed symbols holding each uuid: a paste or a folio copy + //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; + QHash m_folio_titleblocks; + QHash> m_placed_folios; + QHash m_conductor_properties; + //The rows of conductor_properties, to write only what changes + QHash> m_conductor_attributes; + QSet m_dirty_conductor_properties; + int m_building_folios = 0; + QSet m_folios_with_unjoined_wires; + //The folios whose rows a rebuild keeps (keepUnbuiltFolioRows()) + QSet m_kept_folios; + //The tables' rows were filled from the document: those of a + //folio not built yet are right (QET_LAZY_FOLIOS) + bool m_rows_from_document = false; + void keepUnbuiltFolioRows(); + void restoreKeptRows(const QString &table, Diagram *folio, const QString &where); + void dropKeptRows(); + bool m_changed_before_folio = false, m_blocked_before_folio = false; + QHash m_prefilled_elements; + QHash m_prefilled_folios; + QHash m_prefilled_conductors; + struct PrefillTally { + int agree = 0, differ = 0, unplaced = 0; + QMap fields; + }; + PrefillTally m_prefill_tally[3]; + void flushConductorProperties(); + QMultiHash> m_placed_conductors; + bool storeConductorProperties(const QUuid &conductor, const ConductorProperties &properties); + void placeConductor(Conductor *conductor); + void unplaceConductor(Conductor *conductor, const QUuid &uuid); + Conductor *otherConductorHolder(const QUuid &uuid, const Conductor *conductor) const; + void writeConductorRow(Conductor *conductor); + void removeConductorRow(const QUuid &uuid); + void unplaceFolio(Diagram *folio, const QUuid &uuid); + void placeElement(Element *element); + Element *otherHolder(const QUuid &uuid, const Element *element) const; + void writeElementRows(Element *element); + void queueLinks(Element *element); + void removeElementRows(const QUuid &uuid); + void unplaceElement(Element *element, const QUuid &uuid); #ifdef QET_EXPORT_PROJECT_DB public: diff --git a/sources/diagram.cpp b/sources/diagram.cpp index a04d4b433..9586b9555 100644 --- a/sources/diagram.cpp +++ b/sources/diagram.cpp @@ -16,6 +16,7 @@ along with QElectroTech. If not, see . */ #include "diagram.h" +#include "dataBase/projectdatabase.h" #include "autoNum/elementautonumschemecommand.h" #include "ElementsCollection/elementcollectionhandler.h" @@ -193,6 +194,7 @@ Diagram::Diagram(QETProject *project) : * http://tech-artists.org/t/qt-properly-removing-qgraphicitems/3063 */ + border_and_titleblock.setOwnerFolio(this); qgi_manager_ = new QGIManager(this); setBackgroundBrush(Qt::white); conductor_setter_ = new QGraphicsLineItem(nullptr); @@ -767,6 +769,49 @@ void Diagram::keyReleaseEvent(QKeyEvent *e) } } +/** + @brief Diagram::ensureLoaded + Build this folio's items from the XML kept when the project was + opened with QET_LAZY_FOLIOS, and do for them what opening does for + every folio's (QETProject::folioLoaded()). Nothing for a folio built + already. +*/ +void Diagram::ensureLoaded() +{ + if (m_deferred.isNull()) { + return; + } + if (m_project) { + m_project->buildFolios({this}); + } else { + buildDeferred(); + } +} + +/** + @brief Diagram::buildDeferred + Build this folio's items from the XML kept for later, and nothing else. + Their rows are in the project database already, filled from the same + XML: building writes none. + @return true if there was something to build +*/ +bool Diagram::buildDeferred() +{ + if (m_deferred.isNull()) { + return false; + } + QDomElement xml = m_deferred; + m_deferred = QDomElement(); + m_kept_text.clear(); + projectDataBase *db = m_project ? m_project->dataBase() : nullptr; + if (db) db->setBuildingFolio(true); + m_loading_deferred = true; + fromXml(xml, QPointF(), true, nullptr); + m_loading_deferred = false; + if (db) db->setBuildingFolio(false); + return true; +} + /** @brief Diagram::uuid @return the uuid of this diagram @@ -801,7 +846,7 @@ bool Diagram::uuidUsedByOtherDiagram(const QUuid &uuid) const if (!m_project) { return false; } - const auto diagrams = m_project->diagrams(); + const auto diagrams = m_project->folios(); //uuids only: no need to build them for (const Diagram *diagram : diagrams) { if (diagram != this && diagram->m_uuid == uuid) { return true; @@ -838,7 +883,7 @@ QUuid Diagram::derivedUuid(const QDomElement &root, const QString &reason) const QStringLiteral("{d5951240-154d-44d6-8277-0092a31d1920}")); const int index = m_project - ? m_project->diagrams().indexOf(const_cast(this)) + ? m_project->folios().indexOf(const_cast(this)) : -1; const QString project_title = root.ownerDocument() .documentElement() @@ -1088,6 +1133,10 @@ QList < QSet > Diagram::potentials() \~French Un Document XML (QDomDocument) */ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) { + //A folio of a project opened with QET_LAZY_FOLIOS is written from + //its built items (QETProject::toXml() may write one not built as + //it was read instead) + ensureLoaded(); // document QDomDocument document; @@ -1098,120 +1147,7 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) { // schema properties // proprietes du schema if (whole_content) { - //Persist the folio identity, so that a folio keeps the same uuid - //across save/load. Without it every load invents a new one, and - //nothing outside the running instance (version control, a lock, - //a diff tool...) can tell which folio is which. - dom_root.setAttribute(QStringLiteral("uuid"), m_uuid.toString()); - border_and_titleblock.titleBlockToXml(dom_root); - border_and_titleblock.borderToXml(dom_root); - - // Default conductor properties - QDomElement default_conductor = - document.createElement(QStringLiteral("defaultconductor")); - defaultConductorProperties.toXml(default_conductor); - dom_root.appendChild(default_conductor); - - // Conductor autonum - if (!m_conductors_autonum_name.isEmpty()) { - dom_root.setAttribute(QStringLiteral("conductorAutonum"), - m_conductors_autonum_name); - } - - //Default New Element - dom_root.setAttribute(QStringLiteral("freezeNewElement"), - m_freeze_new_elements ? QStringLiteral("true") : QStringLiteral("false")); - - //Default New Conductor - dom_root.setAttribute(QStringLiteral("freezeNewConductor"), - m_freeze_new_conductors_ - ? QStringLiteral("true") : QStringLiteral("false")); - - //Element Folio Sequential Variables - if (!m_elmt_unitfolio_max.isEmpty() - || !m_elmt_tenfolio_max.isEmpty() - || !m_elmt_hundredfolio_max.isEmpty()) { - QDomElement elmtfoliosequential = - document.createElement( - QStringLiteral("elementautonumfoliosequentials")); - if (!m_elmt_unitfolio_max.isEmpty()) { - QDomElement elmtfolioseq = - document.createElement( - QStringLiteral("elementunitfolioseq")); - folioSequentialsToXml(&m_elmt_unitfolio_max, - &elmtfolioseq, - QStringLiteral("sequf_"), - QStringLiteral("unitfolioseq"), - &document); - elmtfoliosequential.appendChild(elmtfolioseq); - } - if (!m_elmt_tenfolio_max.isEmpty()) { - QDomElement elmtfolioseq = - document.createElement( - QStringLiteral("elementtenfolioseq")); - folioSequentialsToXml(&m_elmt_tenfolio_max, - &elmtfolioseq, - QStringLiteral("seqtf_"), - QStringLiteral("tenfolioseq"), - &document); - elmtfoliosequential.appendChild(elmtfolioseq); - } - if (!m_elmt_hundredfolio_max.isEmpty()) { - QDomElement elmtfolioseq = - document.createElement( - QStringLiteral("elementhundredfolioseq")); - folioSequentialsToXml(&m_elmt_hundredfolio_max, - &elmtfolioseq, - QStringLiteral("seqhf_"), - QStringLiteral("hundredfolioseq"), - &document); - elmtfoliosequential.appendChild(elmtfolioseq); - } - dom_root.appendChild(elmtfoliosequential); - } - // Conductor Folio Sequential Variables - if (!m_cnd_unitfolio_max.isEmpty() - || !m_cnd_tenfolio_max.isEmpty() || - !m_cnd_hundredfolio_max.isEmpty()) { - QDomElement cndfoliosequential = - document.createElement( - QStringLiteral("conductorautonumfoliosequentials")); - QHash::iterator i; - if (!m_cnd_unitfolio_max.isEmpty()) { - QDomElement cndfolioseq = - document.createElement( - QStringLiteral("conductorunitfolioseq")); - folioSequentialsToXml(&m_cnd_unitfolio_max, - &cndfolioseq, - QStringLiteral("sequf_"), - QStringLiteral("unitfolioseq"), - &document); - cndfoliosequential.appendChild(cndfolioseq); - } - if (!m_cnd_tenfolio_max.isEmpty()) { - QDomElement cndfolioseq = - document.createElement( - QStringLiteral("conductortenfolioseq")); - folioSequentialsToXml(&m_cnd_tenfolio_max, - &cndfolioseq, - QStringLiteral("seqtf_"), - QStringLiteral("tenfolioseq"), - &document); - cndfoliosequential.appendChild(cndfolioseq); - } - if (!m_cnd_hundredfolio_max.isEmpty()) { - QDomElement cndfolioseq = - document.createElement( - QStringLiteral("conductorhundredfolioseq")); - folioSequentialsToXml(&m_cnd_hundredfolio_max, - &cndfolioseq, - QStringLiteral("seqhf_"), - QStringLiteral("hundredfolioseq"), - &document); - cndfoliosequential.appendChild(cndfolioseq); - } - dom_root.appendChild(cndfoliosequential); - } + writeHeader(document, dom_root); } else { //this method with whole_content to false, @@ -1307,8 +1243,15 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) { // since those ids are assigned sequentially in element order. std::stable_sort(list_elements.begin(), list_elements.end(), [](Element *a, Element *b) { return elementSortKey(a) < elementSortKey(b); }); + //Wires that tie on their ends (common: several from one terminal) + //keep the order they were made in, which for a project's is its + //file's -- the order the scene gave them so far, except for a folio + //built late (QET_LAZY_FOLIOS), whose scene order differed std::stable_sort(list_conductors.begin(), list_conductors.end(), - [](Conductor *a, Conductor *b) { return conductorSortKey(a) < conductorSortKey(b); }); + [](Conductor *a, Conductor *b) { + const QString ka = conductorSortKey(a), kb = conductorSortKey(b); + return ka != kb ? ka < kb : a->creationRank() < b->creationRank(); + }); // A copy carries the numberings its elements follow: pasted into // another project, which does not know them, it can offer to import @@ -1375,6 +1318,143 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) { return(document); } +/** + @brief Diagram::writeHeader + Write this folio's own data -- its uuid, title block, border, default + wire, numbering -- into @p dom_root, the folio's element of @p document: + what toXml() writes before the folio's items. +*/ +void Diagram::writeHeader(QDomDocument &document, QDomElement &dom_root) +{ + //Persist the folio identity, so that a folio keeps the same uuid + //across save/load. Without it every load invents a new one, and + //nothing outside the running instance (version control, a lock, + //a diff tool...) can tell which folio is which. + dom_root.setAttribute(QStringLiteral("uuid"), m_uuid.toString()); + border_and_titleblock.titleBlockToXml(dom_root); + border_and_titleblock.borderToXml(dom_root); + + // Default conductor properties + QDomElement default_conductor = + document.createElement(QStringLiteral("defaultconductor")); + defaultConductorProperties.toXml(default_conductor); + dom_root.appendChild(default_conductor); + + // Conductor autonum + if (!m_conductors_autonum_name.isEmpty()) { + dom_root.setAttribute(QStringLiteral("conductorAutonum"), + m_conductors_autonum_name); + } + + //Default New Element + dom_root.setAttribute(QStringLiteral("freezeNewElement"), + m_freeze_new_elements ? QStringLiteral("true") : QStringLiteral("false")); + + //Default New Conductor + dom_root.setAttribute(QStringLiteral("freezeNewConductor"), + m_freeze_new_conductors_ + ? QStringLiteral("true") : QStringLiteral("false")); + + //Element Folio Sequential Variables + if (!m_elmt_unitfolio_max.isEmpty() + || !m_elmt_tenfolio_max.isEmpty() + || !m_elmt_hundredfolio_max.isEmpty()) { + QDomElement elmtfoliosequential = + document.createElement( + QStringLiteral("elementautonumfoliosequentials")); + if (!m_elmt_unitfolio_max.isEmpty()) { + QDomElement elmtfolioseq = + document.createElement( + QStringLiteral("elementunitfolioseq")); + folioSequentialsToXml(&m_elmt_unitfolio_max, + &elmtfolioseq, + QStringLiteral("sequf_"), + QStringLiteral("unitfolioseq"), + &document); + elmtfoliosequential.appendChild(elmtfolioseq); + } + if (!m_elmt_tenfolio_max.isEmpty()) { + QDomElement elmtfolioseq = + document.createElement( + QStringLiteral("elementtenfolioseq")); + folioSequentialsToXml(&m_elmt_tenfolio_max, + &elmtfolioseq, + QStringLiteral("seqtf_"), + QStringLiteral("tenfolioseq"), + &document); + elmtfoliosequential.appendChild(elmtfolioseq); + } + if (!m_elmt_hundredfolio_max.isEmpty()) { + QDomElement elmtfolioseq = + document.createElement( + QStringLiteral("elementhundredfolioseq")); + folioSequentialsToXml(&m_elmt_hundredfolio_max, + &elmtfolioseq, + QStringLiteral("seqhf_"), + QStringLiteral("hundredfolioseq"), + &document); + elmtfoliosequential.appendChild(elmtfolioseq); + } + dom_root.appendChild(elmtfoliosequential); + } + // Conductor Folio Sequential Variables + if (!m_cnd_unitfolio_max.isEmpty() + || !m_cnd_tenfolio_max.isEmpty() || + !m_cnd_hundredfolio_max.isEmpty()) { + QDomElement cndfoliosequential = + document.createElement( + QStringLiteral("conductorautonumfoliosequentials")); + QHash::iterator i; + if (!m_cnd_unitfolio_max.isEmpty()) { + QDomElement cndfolioseq = + document.createElement( + QStringLiteral("conductorunitfolioseq")); + folioSequentialsToXml(&m_cnd_unitfolio_max, + &cndfolioseq, + QStringLiteral("sequf_"), + QStringLiteral("unitfolioseq"), + &document); + cndfoliosequential.appendChild(cndfolioseq); + } + if (!m_cnd_tenfolio_max.isEmpty()) { + QDomElement cndfolioseq = + document.createElement( + QStringLiteral("conductortenfolioseq")); + folioSequentialsToXml(&m_cnd_tenfolio_max, + &cndfolioseq, + QStringLiteral("seqtf_"), + QStringLiteral("tenfolioseq"), + &document); + cndfoliosequential.appendChild(cndfolioseq); + } + if (!m_cnd_hundredfolio_max.isEmpty()) { + QDomElement cndfolioseq = + document.createElement( + QStringLiteral("conductorhundredfolioseq")); + folioSequentialsToXml(&m_cnd_hundredfolio_max, + &cndfolioseq, + QStringLiteral("seqhf_"), + QStringLiteral("hundredfolioseq"), + &document); + cndfoliosequential.appendChild(cndfolioseq); + } + dom_root.appendChild(cndfoliosequential); + } +} + +/** + @brief Diagram::headerXml + @return the folio's own data as writeHeader() writes it, as text +*/ +QString Diagram::headerXml() +{ + QDomDocument document; + auto dom_root = document.createElement(QStringLiteral("diagram")); + document.appendChild(dom_root); + writeHeader(document, dom_root); + return document.toString(); +} + /** @brief Diagram::folioSequentialsToXml Add folio sequential to QDomElement @@ -1692,8 +1772,9 @@ bool Diagram::fromXml(QDomElement &document, return(false); } - // Read attributes of this diagram - if (consider_informations) + // Read attributes of this diagram (already read for a folio whose + // items are built later, see ensureLoaded()) + if (consider_informations && !m_loading_deferred) { // Restore the persisted folio uuid. Done first, before any item is // loaded, so that everything created below sees the final uuid. @@ -1701,6 +1782,7 @@ bool Diagram::fromXml(QDomElement &document, // persisted, or uuid already taken by another folio) gets a // deterministic one instead, see derivedUuid(). const QUuid persisted_uuid(root.attribute(QStringLiteral("uuid"))); + const QUuid old_uuid = m_uuid; if (persisted_uuid.isNull()) { m_uuid = derivedUuid(root, QStringLiteral("legacy")); } else if (uuidUsedByOtherDiagram(persisted_uuid)) { @@ -1709,6 +1791,9 @@ bool Diagram::fromXml(QDomElement &document, } else { m_uuid = persisted_uuid; } + //A placed folio's stored title block follows its uuid + if (m_project && m_project->dataBase()) + m_project->dataBase()->folioUuidChanged(this, old_uuid); // Load border and titleblock border_and_titleblock.titleBlockFromXml(root); @@ -1776,6 +1861,16 @@ bool Diagram::fromXml(QDomElement &document, return(true); } + //A project opened with QET_LAZY_FOLIOS builds a folio's items when + //they are first needed: until then the folio has what it says + //about itself (uuid, title block, border, numbering) and keeps its + //XML, which belongs to the project's document. + if (consider_informations && !m_loading_deferred && m_project + && m_project->deferringFolios()) { + m_deferred = root; + return(true); + } + //If paste from another project if (root.hasAttribute(QStringLiteral("projectId"))) { diff --git a/sources/diagram.h b/sources/diagram.h index c19cac53b..6b2e6c604 100644 --- a/sources/diagram.h +++ b/sources/diagram.h @@ -140,6 +140,11 @@ class Diagram : public QGraphicsScene bool m_freeze_new_elements; bool m_freeze_new_conductors_; QUuid m_uuid = QUuid::createUuid(); + /// The XML of this folio's items, while they are not built yet + QDomElement m_deferred; + QString m_opened_header; + QByteArray m_kept_text; + bool m_loading_deferred = false; //Selection before the current click, see completeGroupSelection() QList> m_previous_selection; @@ -200,10 +205,26 @@ class Diagram : public QGraphicsScene // methods related to XML import/export QDomDocument toXml(bool wholeContent = true, bool is_copy_command = false); + void writeHeader(QDomDocument &document, QDomElement &dom_root); bool initFromXml(QDomElement &, QPointF = QPointF(), bool = true, DiagramContent * = nullptr); + /// false while this folio's items wait in its XML to be + /// built (QET_LAZY_FOLIOS, LAZY-FOLIO-PLAN.md) + bool isLoaded() const {return m_deferred.isNull();} + QDomElement deferredXml() const {return m_deferred;} + QString headerXml(); + /// The folio's text in the file it was read from, while it is + /// not built (QETProject::toXml()) + QByteArray keptText() const {return m_kept_text;} + void setKeptText(const QByteArray &text) {m_kept_text = text;} + /// Remember the folio's own data as it is now, for + /// headerChanged() (QETProject::toXml()) + void rememberHeader() {m_opened_header = headerXml();} + bool headerChanged() {return headerXml() != m_opened_header;} + void ensureLoaded(); + bool buildDeferred(); //The last argument is only for a copy fragment: when it is //given, the cable lines that fragment carries are built as //well and handed back through it -- they are not part of a diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index 548606aa0..5a1bc6590 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -1645,6 +1645,25 @@ void DiagramView::editDiagramProperties() DiagramPropertiesDialog::diagramPropertiesDialog(m_diagram, diagramEditor()); } +/** + @brief DiagramView::buildFolio + Build the items of the folio this view shows, and those of the folios + its cross-references need, if the project was opened without them + (QET_LAZY_FOLIOS); then fit the view to them. +*/ +void DiagramView::buildFolio() +{ + if (QETProject *project = m_diagram->project()) { + if (!project->unloadedFolioCount()) { + return; + } + project->buildFolioToShow(m_diagram); + } else { + m_diagram->ensureLoaded(); + } + adjustSceneRect(); +} + /** @brief DiagramView::adjustSceneRect Calcul and set the area of the scene visualized by this view diff --git a/sources/diagramview.h b/sources/diagramview.h index 4a3601ed7..2453e7b4b 100644 --- a/sources/diagramview.h +++ b/sources/diagramview.h @@ -207,6 +207,7 @@ class DiagramView : public PaletteGraphicsView void paste(const QPointF & = QPointF(), QClipboard::Mode = QClipboard::Clipboard); void pasteHere(); void duplicate(const QPoint &stepOffset); + void buildFolio(); void adjustSceneRect(); void updateWindowTitle(); void resetConductors(); diff --git a/sources/elementprovider.cpp b/sources/elementprovider.cpp index 391bedc3a..a493fd7fe 100644 --- a/sources/elementprovider.cpp +++ b/sources/elementprovider.cpp @@ -30,11 +30,10 @@ @param prj the project where we must find element @param diagram the diagram to exclude from the search */ -ElementProvider::ElementProvider(QETProject *prj, Diagram *diagram) -{ - m_diagram_list = prj->diagrams(); - m_diagram_list.removeOne(diagram); -} +ElementProvider::ElementProvider(QETProject *prj, Diagram *diagram) : + m_project(prj), + m_excluded(diagram) +{} /** @brief ElementProvider::ElementProvider Constructor @@ -44,6 +43,46 @@ ElementProvider::ElementProvider(Diagram *diag) { m_diagram_list << diag; } +/** + @brief ElementProvider::ElementProvider Constructor + @param diagrams the diagrams to search, e.g. QETProject::builtFolios() +*/ +ElementProvider::ElementProvider(const QList &diagrams) : + m_diagram_list(diagrams) +{} + +/** + @brief ElementProvider::folios + @return the folios to search for symbols of the kinds @p kinds (free + ones only if @p free_only). For a project, the folios not built yet + (QET_LAZY_FOLIOS) holding such a symbol are built first, and only them + (QETProject::buildFoliosHolding()). +*/ +QList ElementProvider::folios(ElementData::Types kinds, bool free_only) const +{ + if (!m_project) { + return m_diagram_list; + } + m_project->buildFoliosHolding(kinds, free_only); + QList list = m_project->builtFolios(); + list.removeOne(m_excluded); + return list; +} + +/** + @brief ElementProvider::allFolios + @return the folios to search for anything else: every one, built +*/ +QList ElementProvider::allFolios() const +{ + if (!m_project) { + return m_diagram_list; + } + QList list = m_project->diagrams(); + list.removeOne(m_excluded); + return list; +} + /** @brief ElementProvider::FreeElement Search and return the asked element corresponding with the given filter @@ -57,16 +96,22 @@ QVector > ElementProvider::freeElement(ElementData::Types filt { QVector> free_elmt; QList elmt_list; + const QList searched = folios(filter, true); + //A symbol whose saved partner is on a folio not built yet + //(QET_LAZY_FOLIOS) is not linked yet, but is not free: it will be + const QSet waiting = m_project ? m_project->symbolsOnUnbuiltFolios() + : QSet(); //search in all diagram - for (const auto &diagram_ : std::as_const(m_diagram_list)) + for (const auto &diagram_ : std::as_const(searched)) { //get all element in diagram d elmt_list = diagram_->elements(); for (const auto &elmt_ : std::as_const(elmt_list)) { if (filter & elmt_->elementData().m_type && - elmt_->isFree()) + elmt_->isFree() && + !elmt_->pendingLinks().intersects(waiting)) { free_elmt << elmt_; } @@ -86,7 +131,7 @@ QList ElementProvider::fromUuids(QList uuid_list) const { QList found_element; - foreach (Diagram *d, m_diagram_list) { + foreach (Diagram *d, allFolios()) { foreach(Element *elmt, d->elements()) { if (uuid_list.contains(elmt->uuid())) { found_element << elmt; @@ -106,7 +151,8 @@ QList ElementProvider::fromUuids(QList uuid_list) const QVector> ElementProvider::find(ElementData::Types elmt_type) const { QVector> returned_vector; - for (const auto &diagram_ : std::as_const(m_diagram_list)) + const QList searched = folios(elmt_type, false); + for (const auto &diagram_ : std::as_const(searched)) { const auto elmt_list = diagram_->elements(); for (const auto &elmt_ : elmt_list) @@ -153,7 +199,7 @@ QVector ElementProvider::table( } } - for (auto d : m_diagram_list) { + for (auto d : allFolios()) { for (auto item_ : d->items()) { if(item_->type() == QetGraphicsTableItem::Type) @@ -197,8 +243,9 @@ QetGraphicsTableItem *ElementProvider::tableFromUuid(const QUuid &uuid) QVector ElementProvider::freeTerminal() const { QVector vector_; + const QList searched = folios(ElementData::Terminal, false); - for (const auto &diagram : std::as_const(m_diagram_list)) + for (const auto &diagram : std::as_const(searched)) { const auto elmt_list{diagram->elements()}; diff --git a/sources/elementprovider.h b/sources/elementprovider.h index 2660b1cde..5f839c38c 100644 --- a/sources/elementprovider.h +++ b/sources/elementprovider.h @@ -41,6 +41,7 @@ class ElementProvider public: ElementProvider(QETProject *prj, Diagram *diagram=nullptr); ElementProvider(Diagram *diag); + explicit ElementProvider(const QList &diagrams); QVector > freeElement(ElementData::Types filter) const; QList fromUuids(QList ) const; QVector > find(ElementData::Types elmt_type) const; @@ -49,7 +50,15 @@ class ElementProvider QVector freeTerminal() const; private: + QList folios(ElementData::Types kinds, bool free_only) const; + QList allFolios() const; + QList m_diagram_list; + //Searching a project: its folios are asked for when searched, + //so that a project opened with QET_LAZY_FOLIOS builds only + //those a search can find something on + QETProject *m_project = nullptr; + Diagram *m_excluded = nullptr; }; #endif // ELEMENTPROVIDER_H diff --git a/sources/elementspanelwidget.cpp b/sources/elementspanelwidget.cpp index 3b44d6380..fb5e5e7fa 100644 --- a/sources/elementspanelwidget.cpp +++ b/sources/elementspanelwidget.cpp @@ -473,14 +473,16 @@ void ElementsPanelWidget::updateButtons() if (!selected_diagrams.isEmpty()) { QETProject *selected_diagram_project = selected_diagrams.first() -> project(); bool is_writable = !(selected_diagram_project -> isReadOnly()); - int project_diagrams_count = selected_diagram_project -> diagrams().count(); + //Positions only: folios() does not build the folios' items + const QList folios = selected_diagram_project -> folios(); + int project_diagrams_count = folios.count(); // Find the highest (min) and lowest (max) index among the selection int min_position = project_diagrams_count; int max_position = -1; for (Diagram *diagram : selected_diagrams) { - int pos = selected_diagram_project -> diagrams().indexOf(diagram); + int pos = folios.indexOf(diagram); if (pos < min_position) min_position = pos; if (pos > max_position) max_position = pos; } diff --git a/sources/genericpanel.cpp b/sources/genericpanel.cpp index fe4b44b6a..a11d43ac6 100644 --- a/sources/genericpanel.cpp +++ b/sources/genericpanel.cpp @@ -253,11 +253,13 @@ QTreeWidgetItem *GenericPanel::fillProjectItem(QTreeWidgetItem *project_qtwi, this,&GenericPanel::projectDiagramsOrderChanged); } else { // remove diagrams unknown to the project (presumably removed) - removeObsoleteItems(project -> diagrams(), + removeObsoleteItems(project -> folios(), project_qtwi, QET::Diagram, false); } int index = 0; - foreach (Diagram *diagram, project -> diagrams()) { + //A folio's entry shows its own data (title, folio label): + //folios(), which does not build the folios' items + foreach (Diagram *diagram, project -> folios()) { QTreeWidgetItem *diagram_qtwi = addDiagram(diagram, nullptr, options); diff --git a/sources/print/projectprintwindow.cpp b/sources/print/projectprintwindow.cpp index 41912c368..e04ea1d44 100644 --- a/sources/print/projectprintwindow.cpp +++ b/sources/print/projectprintwindow.cpp @@ -208,7 +208,7 @@ void ProjectPrintWindow::requestPaint() #endif #endif - if (!m_project->diagrams().count()) { + if (!m_project->folios().count()) { return; } diff --git a/sources/projectview.cpp b/sources/projectview.cpp index 81ad2a6f7..47bdc0719 100644 --- a/sources/projectview.cpp +++ b/sources/projectview.cpp @@ -959,7 +959,9 @@ void ProjectView::loadDiagrams() { if (!m_project) return; - setDisplayFallbackWidget(m_project -> diagrams().isEmpty()); + //The folios' items are built when their tab is first shown + //(tabChanged()), not here: a tab needs only the folio's own data + setDisplayFallbackWidget(m_project -> folios().isEmpty()); auto dialog = DialogWaiting::instance(); if(dialog) @@ -968,7 +970,7 @@ void ProjectView::loadDiagrams() "...
Creating sheet tabs :

")); } - for(auto diagram : m_project->diagrams()) + for(auto diagram : m_project->folios()) { if(dialog) { @@ -1202,6 +1204,12 @@ void ProjectView::rebuildDiagramsMap() */ void ProjectView::tabChanged(int tab_id) { + //A folio opened without its items (QET_LAZY_FOLIOS) is built when + //its tab is first shown. Read the tab itself: the first tab is + //shown while loadDiagrams() is still filling m_diagram_ids + if (auto dv = qobject_cast(m_tab->widget(tab_id))) + dv->buildFolio(); + if (tab_id == -1) setDisplayFallbackWidget(true); else if(m_tab->count() == 1) diff --git a/sources/qet.cpp b/sources/qet.cpp index daeef58b4..0f7a1c3af 100644 --- a/sources/qet.cpp +++ b/sources/qet.cpp @@ -770,6 +770,15 @@ bool QET::compareCanonicalFilePaths(const QString &first, const QString &second) @return false if an error occurred, true otherwise */ bool QET::writeXmlFile(QDomDocument &xml_doc, const QString &filepath, QString *error_message) +{ + return writeXmlText(xml_doc.toString(4), filepath, error_message); +} + +/** + @brief QET::writeXmlText + writeXmlFile() for a document already written as @p text +*/ +bool QET::writeXmlText(const QString &text, const QString &filepath, QString *error_message) { QSaveFile file(filepath); @@ -789,7 +798,7 @@ bool QET::writeXmlFile(QDomDocument &xml_doc, const QString &filepath, QString * QTextStream out(&file); out.setEncoding(QStringConverter::Utf8); out.setGenerateByteOrderMark(false); - out << xml_doc.toString(4); + out << text; if (!file.commit()) { if (error_message) { @@ -895,6 +904,15 @@ QActionGroup *QET::depthActionGroup(QObject *parent) } bool QET::writeToFile(QDomDocument &xml_doc, QFile *file, QString *error_message) +{ + return writeTextToFile(xml_doc.toString(4), file, error_message); +} + +/** + @brief QET::writeTextToFile + writeToFile() for a document already written as @p text +*/ +bool QET::writeTextToFile(const QString &text, QFile *file, QString *error_message) { bool opened_here = file->isOpen() ? false : true; @@ -920,7 +938,7 @@ bool QET::writeToFile(QDomDocument &xml_doc, QFile *file, QString *error_message out.seek(0); out.setEncoding(QStringConverter::Utf8); out.setGenerateByteOrderMark(false); - out << xml_doc.toString(4); + out << text; if (opened_here) { file->close(); } diff --git a/sources/qet.h b/sources/qet.h index 6901446a0..630ede6e8 100644 --- a/sources/qet.h +++ b/sources/qet.h @@ -202,6 +202,8 @@ namespace QET { bool compareCanonicalFilePaths(const QString &, const QString &); bool writeXmlFile(QDomDocument &xml_doc, const QString &filepath, QString * error_message= nullptr); bool writeToFile (QDomDocument &xml_doc, QFile *file, QString *error_message = nullptr); + bool writeXmlText(const QString &text, const QString &filepath, QString *error_message = nullptr); + bool writeTextToFile(const QString &text, QFile *file, QString *error_message = nullptr); bool eachStrIsEqual (const QStringList &qsl); QActionGroup *depthActionGroup(QObject *parent = nullptr); void saveCustomColors(); diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index 907ae692b..8c9924606 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -562,8 +562,9 @@ void QETDiagramEditor::setUpActions() m_draw_grid->setCheckable(true); m_draw_grid->setChecked(settings.value("diagrameditor/grid_display_startup", true).toBool()); connect(m_draw_grid, &QAction::triggered, [this](bool checked) { + //The folio's own setting: folios() builds nothing foreach (ProjectView *prjv, this->openedProjects()) - foreach (Diagram *d, prjv->project()->diagrams()) { + foreach (Diagram *d, prjv->project()->folios()) { d->setDisplayGrid(checked); d->update(); } @@ -603,8 +604,9 @@ void QETDiagramEditor::setUpActions() m_draw_guides->setCheckable(true); m_draw_guides->setChecked(settings.value("diagrameditor/guides_display_startup", false).toBool()); connect(m_draw_guides, &QAction::triggered, [this](bool checked) { + //The folio's own setting: folios() builds nothing foreach (ProjectView *prjv, this->openedProjects()) - foreach (Diagram *d, prjv->project()->diagrams()) { + foreach (Diagram *d, prjv->project()->folios()) { d->setDisplayGuides(checked); } }); @@ -652,8 +654,10 @@ void QETDiagramEditor::setUpActions() action->setData(int(kind.first)); connect(action, &QAction::triggered, this, [kind](bool checked) { ShownKinds::setShown(kind.first, checked); + //Items built later are shown or hidden as they are made + //(ShownKinds::tag()) for (QETProject *project : QETApp::registeredProjects()) - for (Diagram *diagram : project->diagrams()) + for (Diagram *diagram : project->builtFolios()) ShownKinds::apply(diagram, kind.first); emit QETApp::instance()->shownKindsChanged(); }); @@ -2004,7 +2008,7 @@ bool QETDiagramEditor::openAndAddProject( //was not found: they would otherwise vanish on the next save //without the user ever being told. QStringList lost_wires; - for (Diagram *diagram : project->diagrams()) { + for (Diagram *diagram : project->foliosWithWiresNotReconnected()) { for (const QString &wire : diagram->wiresNotReconnected()) { lost_wires << tr("Sheet %1: %2").arg(diagram->folioIndex() + 1).arg(wire); } diff --git a/sources/qetgraphicsitem/ViewItem/projectdbmodel.cpp b/sources/qetgraphicsitem/ViewItem/projectdbmodel.cpp index b8c83334b..cf9525cc2 100644 --- a/sources/qetgraphicsitem/ViewItem/projectdbmodel.cpp +++ b/sources/qetgraphicsitem/ViewItem/projectdbmodel.cpp @@ -37,7 +37,8 @@ */ ProjectDBModel::ProjectDBModel(QETProject *project, QObject *parent) : QAbstractTableModel(parent), - m_project(project) + m_project(project), + m_fill_when_built(project->dataBase()->isBuildingFolio()) { connect(m_project->dataBase(), &projectDataBase::dataBaseUpdated, this, &ProjectDBModel::dataBaseUpdated); } @@ -320,6 +321,21 @@ void ProjectDBModel::setIdentifier(const QString &identifier) { m_identifier = identifier; } +/** + @brief ProjectDBModel::folioBuilt + The folio of a table made while it was being built (QET_LAZY_FOLIOS) + is built: read the database, as opening does once it has filled it. + Nothing for any other model. +*/ +void ProjectDBModel::folioBuilt() +{ + if (!m_fill_when_built) { + return; + } + m_fill_when_built = false; + dataBaseUpdated(); +} + /** @brief ProjectDBModel::dataBaseUpdated slot called when the project database is updated @@ -374,6 +390,13 @@ void ProjectDBModel::setHeaderString() void ProjectDBModel::fillValue() { m_record.clear(); + //Opening builds a table before the database is filled, and fills + //it after: a table on a folio built late (QET_LAZY_FOLIOS) does + //the same, so that it is laid out as it would have been on + //opening -- empty until its folio is built (folioBuilt()) + if (m_fill_when_built) { + return; + } auto query_ = m_project->dataBase()->newQuery(m_query); if (!query_.exec()) { diff --git a/sources/qetgraphicsitem/ViewItem/projectdbmodel.h b/sources/qetgraphicsitem/ViewItem/projectdbmodel.h index add4db1da..37385765c 100644 --- a/sources/qetgraphicsitem/ViewItem/projectdbmodel.h +++ b/sources/qetgraphicsitem/ViewItem/projectdbmodel.h @@ -71,6 +71,7 @@ class ProjectDBModel : public QAbstractTableModel void setIdentifier(const QString &identifier); QString identifier() const {return m_identifier;} static QString xmlTagName() {return QString("project_data_base_model");} + void folioBuilt(); private: void dataBaseUpdated(); @@ -79,6 +80,7 @@ class ProjectDBModel : public QAbstractTableModel private: QPointer m_project; + bool m_fill_when_built = false; QString m_query; QVector m_record; //First int = section, second int = Qt::role, QVariant = value diff --git a/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp b/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp index 7a4f50155..0b6922ebf 100644 --- a/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp +++ b/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp @@ -565,15 +565,34 @@ void QetGraphicsTableItem::initLink() { if (!m_pending_previous_table_uuid.isNull()) { - ElementProvider provider_(this->diagram()->project()); - if (auto previous_table = provider_.tableFromUuid(m_pending_previous_table_uuid)) { + //The built folios only: a previous table on a folio not built + //yet (QET_LAZY_FOLIOS) is linked when that folio is + QETProject *project = this->diagram()->project(); + ElementProvider provider_(project->builtFolios()); + auto previous_table = provider_.tableFromUuid(m_pending_previous_table_uuid); + if (previous_table) { setPreviousTable(previous_table); } - m_pending_previous_table_uuid = QUuid(); //Set to null in case initLink is called again + if (previous_table || !project->unloadedFolioCount()) { + m_pending_previous_table_uuid = QUuid(); //Set to null in case initLink is called again + } } setSize(m_pending_size); } +/** + @brief QetGraphicsTableItem::previousTableUuid + @return the uuid of the table before this one in its chain, linked or + still waiting for its folio to be built (QET_LAZY_FOLIOS); null if none +*/ +QUuid QetGraphicsTableItem::previousTableUuid() const +{ + if (m_previous_table) { + return m_previous_table->uuid(); + } + return m_pending_previous_table_uuid; +} + /** @brief QetGraphicsTableItem::minimumRowHeight @return the minimum height of a row @@ -610,7 +629,7 @@ QDomElement QetGraphicsTableItem::toXml(QDomDocument &dom_document) const const auto project_ = diagram() ? diagram()->project() : nullptr; auto in_project = [project_](const QetGraphicsTableItem *table) { return project_ && table->diagram() - && project_->diagrams().contains(table->diagram()); + && project_->folios().contains(table->diagram()); }; QetGraphicsTableItem *previous_ = m_previous_table; while (previous_ && !in_project(previous_)) { diff --git a/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.h b/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.h index b23fe3244..3b8cf7438 100644 --- a/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.h +++ b/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.h @@ -84,6 +84,8 @@ class QetGraphicsTableItem : public QetGraphicsItem QPointer nextTable() const; void setToMinimumHeight(); void initLink(); + bool hasPendingLink() const {return !m_pending_previous_table_uuid.isNull();} + QUuid previousTableUuid() const; QUuid uuid() const {return m_uuid;} int minimumRowHeight() const; diff --git a/sources/qetgraphicsitem/conductor.cpp b/sources/qetgraphicsitem/conductor.cpp index dc2715139..720db396c 100644 --- a/sources/qetgraphicsitem/conductor.cpp +++ b/sources/qetgraphicsitem/conductor.cpp @@ -25,6 +25,7 @@ #include "../conductorsegment.h" #include "../conductorsegmentprofile.h" #include "../diagram.h" +#include "../dataBase/projectdatabase.h" #include "../diagramcommands.h" #include "../qetdiagrameditor.h" #include "../qetgraphicsitem/terminal.h" @@ -118,6 +119,8 @@ Conductor::Conductor(Terminal *p1, Terminal* p2) : setZValue(11); m_previous_z_value = zValue(); m_uuid = QUuid::createUuid(); + static quint64 created = 0; + m_creation_rank = ++created; //Add this conductor to the list of conductors of each of the two terminals bool ajout_p1 = terminal1 -> addConductor(this); @@ -1114,6 +1117,7 @@ void Conductor::pointsToSegments(const QList& points_list) { */ bool Conductor::fromXml(QDomElement &dom_element) { + const QUuid old_uuid = m_uuid; //Older project files have no conductor uuid attribute at all -- //generate one on load, same treatment terminal uuids got when //that field was introduced (see terminal1/terminal2 handling in @@ -1129,6 +1133,7 @@ bool Conductor::fromXml(QDomElement &dom_element) //single load-and-resave (see #754). m_uuid = QUuid::createUuid(); } + uuidChanged(old_uuid); //"nan" and "inf" parse as numbers; a non-finite position is kept at 0 const qreal x = dom_element.attribute("x", nullptr).toDouble(); @@ -1235,7 +1240,7 @@ QDomElement Conductor::toXml(QDomDocument &dom_document, dom_element.appendChild(dom_seq); // Export the properties and text - m_properties. toXml(dom_element); + properties().toXml(dom_element); if(m_text_item->wasMovedByUser()) { dom_element.setAttribute("userx", QString::number(m_text_item->pos().x())); @@ -1735,6 +1740,7 @@ void Conductor::refreshText() m_properties.text = m_properties.m_formula; m_text_item->setPlainText(m_properties.text); } + storeProperties(); } else { @@ -1833,6 +1839,7 @@ void Conductor::setProperties(const ConductorProperties &property) calculateTextItemPosition(); update(); + storeProperties(); emit propertiesChange(); } @@ -1842,9 +1849,75 @@ void Conductor::setProperties(const ConductorProperties &property) */ ConductorProperties Conductor::properties() const { + if (projectDataBase *store = placedStore()) + return store->conductorProperties(m_uuid); return(m_properties); } +/** + @brief Conductor::placedStore + @return the project database holding this wire's properties + (DB-ACCESSORS-PLAN.md stage 4.5), or nullptr if it is not placed in a + project, or shares its uuid with another wire for the moment a copy + takes to get its own +*/ +projectDataBase *Conductor::placedStore() const +{ + Diagram *d = diagram(); + QETProject *project = d ? d->project() : nullptr; + projectDataBase *store = project ? project->dataBase() : nullptr; + return store && store->hasConductorProperties(m_uuid) + && store->placedConductorCount(m_uuid) == 1 ? store : nullptr; +} + +/** + @brief Conductor::storeProperties + Write this wire's properties to its project's store, once it is placed: + every change of a placed wire's properties comes through here. +*/ +void Conductor::storeProperties() +{ + Diagram *d = diagram(); + QETProject *project = d ? d->project() : nullptr; + if (project && project->dataBase() && project->dataBase()->hasConductorProperties(m_uuid)) + project->dataBase()->conductorPropertiesStored(this, m_properties); +} + +/** + @brief Conductor::newUuid + Make a new uuid for this conductor; its stored properties and its row + follow it. +*/ +void Conductor::newUuid() +{ + setUuid(QUuid::createUuid()); +} + +/** + @brief Conductor::setUuid + @param uuid : the uuid of this conductor, saved from now on +*/ +void Conductor::setUuid(const QUuid &uuid) +{ + const QUuid old_uuid = m_uuid; + m_uuid = uuid; + m_persist_uuid = true; + uuidChanged(old_uuid); +} + +/** + @brief Conductor::uuidChanged + Tell the project's store this placed wire's uuid changed from + @p old_uuid (a paste renews it, a file gives it). +*/ +void Conductor::uuidChanged(const QUuid &old_uuid) +{ + Diagram *d = diagram(); + QETProject *project = d ? d->project() : nullptr; + if (project && project->dataBase()) + project->dataBase()->conductorUuidChanged(this, old_uuid); +} + /** @brief Conductor::setCableReference Record the cable core this conductor shows, together with the label @@ -1868,6 +1941,7 @@ void Conductor::setCableReference(const QString &label, m_properties.m_cable = label; m_properties.m_cable_uuid = cable_uuid; m_properties.m_cable_slot = slot; + storeProperties(); emit propertiesChange(); } diff --git a/sources/qetgraphicsitem/conductor.h b/sources/qetgraphicsitem/conductor.h index f75931064..844e97787 100644 --- a/sources/qetgraphicsitem/conductor.h +++ b/sources/qetgraphicsitem/conductor.h @@ -27,6 +27,7 @@ class ConductorProfile; class ConductorSegmentProfile; class Diagram; +class projectDataBase; class Terminal; class ConductorSegment; class ConductorTextItem; @@ -80,8 +81,8 @@ class Conductor : public QGraphicsObject Diagram *diagram() const; ConductorTextItem *textItem() const; QUuid uuid() const {return m_uuid;} - void newUuid() {m_uuid = QUuid::createUuid(); m_persist_uuid = true;} //create new uuid for this conductor - void setUuid(const QUuid &uuid) {m_uuid = uuid; m_persist_uuid = true;} //saved from now on + void newUuid(); //create new uuid for this conductor + void setUuid(const QUuid &uuid); //saved from now on void updatePath(const QRectF & = QRectF()); //This method do nothing, it's only made to be used with Q_PROPERTY @@ -125,6 +126,11 @@ class Conductor : public QGraphicsObject bool only_text = false); void setProperties(const ConductorProperties &property); ConductorProperties properties() const; + /// This wire's own copy of its properties, whatever the store holds + ConductorProperties ownProperties() const {return m_properties;} + /// The order wires were made in this session: a project's are + /// made in the order its file lists them + quint64 creationRank() const {return m_creation_rank;} /// Records which cable core this conductor shows and writes the /// label that assignment generates (15W1:br) into its cable /// field. Deliberately not routed through @@ -233,6 +239,10 @@ class Conductor : public QGraphicsObject /// file had none -- toXml() must not persist that value, or /// every reload mints and saves a new random one (see #754). bool m_persist_uuid = true; + quint64 m_creation_rank = 0; + void uuidChanged(const QUuid &old_uuid); + projectDataBase *placedStore() const; + void storeProperties(); /// QPen et QBrush objects used to draw conductors static QPen conductor_pen; diff --git a/sources/qetgraphicsitem/element.cpp b/sources/qetgraphicsitem/element.cpp index 56c8bda2d..779c6f7c9 100644 --- a/sources/qetgraphicsitem/element.cpp +++ b/sources/qetgraphicsitem/element.cpp @@ -18,6 +18,7 @@ #include "element.h" #include "../qetapp.h" #include "../qetproject.h" +#include "../dataBase/projectdatabase.h" #include "../PropertiesEditor/propertieseditordialog.h" #include "../autoNum/assignvariables.h" #include "../autoNum/elementautonumschemecommand.h" @@ -718,6 +719,7 @@ bool Element::buildFromXml(const QDomElement &xml_def_elmt, int *state) m_data.m_informations.fromXml( xml_def_elmt.firstChildElement(QStringLiteral("elementInformations")), QStringLiteral("elementInformation")); + storeInformation(); //scroll of the Children of the Definition: Parts of the Drawing int parsed_elements_count = 0; @@ -1042,7 +1044,9 @@ bool Element::fromXml(QDomElement &e, } //uuid of this element - m_uuid = QUuid(e.attribute(QStringLiteral("uuid"), QUuid::createUuid().toString())); + //Through setUuid(): a folio adds a symbol before reading it, so its + //stored information has to follow from the uuid it was born with. + setUuid(QUuid(e.attribute(QStringLiteral("uuid"), QUuid::createUuid().toString()))); //load prefix m_prefix = e.attribute(QStringLiteral("prefix")); @@ -1265,10 +1269,11 @@ QDomElement Element::toXml( } //save information of this element - if (! m_data.m_informations.keys().isEmpty()) { + const DiagramContext informations = elementInformations(); + if (! informations.keys().isEmpty()) { QDomElement infos = document.createElement(QStringLiteral("elementInformations")); - m_data.m_informations.toXml(infos, QStringLiteral("elementInformation")); + informations.toXml(infos, QStringLiteral("elementInformation")); //toXml() skips empty values: an element whose information is //all empty would otherwise be written an empty block, which the //next load reads as no information and the next save drops. @@ -1625,7 +1630,10 @@ void Element::initLink(QETProject *prj) // if nothing to link return now if (tmp_uuids_link.isEmpty()) return; - ElementProvider ep(prj); + //The built folios only: a partner on a folio not built yet + //(QET_LAZY_FOLIOS) is linked when its folio is, and the saved list + //is kept until then, for the order it gives + ElementProvider ep(prj->builtFolios()); QList uuids; for (const auto &linkInfo : tmp_uuids_link) { uuids.append(linkInfo.uuid); @@ -1643,7 +1651,27 @@ void Element::initLink(QETProject *prj) } } restoreSavedLinkOrder(); - tmp_uuids_link.clear(); + if (pendingLinks().isEmpty() || !prj->unloadedFolioCount()) { + tmp_uuids_link.clear(); + } +} + +/** + @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 (QET_LAZY_FOLIOS), + until initLink() runs again once it is +*/ +QSet Element::pendingLinks() const +{ + QSet pending; + for (const auto &link_info : tmp_uuids_link) { + pending.insert(link_info.uuid); + } + for (Element *elmt : connected_elements) { + pending.remove(elmt->uuid()); + } + return pending; } /** @@ -1674,6 +1702,23 @@ void Element::initLink(const QList &candidates) 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) +*/ +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. @@ -1795,6 +1840,33 @@ void Element::setFormulaSchemeId(const QUuid &id) m_data.m_informations.addValue(QETInformation::ELMT_FORMULA_ID, id.toString(), false); } + storeInformation(); +} + +/** + @brief Element::setUuid + A placed symbol's stored information and its database rows follow its + uuid (a paste or a folio copy renews it). +*/ +void Element::setUuid(const QUuid &uuid) +{ + const QUuid old_uuid = m_uuid; + m_uuid = uuid; + if (diagram() && diagram()->project() && diagram()->project()->dataBase()) + diagram()->project()->dataBase()->elementUuidChanged(this, old_uuid); +} + +/** + @brief Element::storeInformation + Write this symbol's information to its project's store, once it is + placed (DB-ACCESSORS-PLAN.md stage 4.1): every change of a placed + symbol's information comes through here. +*/ +void Element::storeInformation() +{ + if (diagram() && diagram()->project() && diagram()->project()->dataBase() + && diagram()->project()->dataBase()->hasElementInformation(m_uuid)) + diagram()->project()->dataBase()->elementInformationChanged(this, m_data.m_informations); } void Element::setElementInformations(DiagramContext dc) @@ -1811,6 +1883,7 @@ void Element::setElementInformations(DiagramContext dc) if (!actual_label.isEmpty()) { m_data.m_informations.addValue(QStringLiteral("label"), actual_label); //Update the label if there is a formula } + storeInformation(); emit elementInfoChange(old_info, m_data.m_informations); // Propagate label change to linked PLC slaves (label is changed via @@ -1841,9 +1914,43 @@ void Element::setElementInformations(DiagramContext dc) */ ElementData Element::elementData() const { + if (projectDataBase *store = placedStore()) { + ElementData data = m_data; + data.m_informations = store->elementInformation(m_uuid); + return data; + } return m_data; } +/** + @brief Element::elementInformations + @return this symbol's information: for a placed symbol the project's + store holds it (DB-ACCESSORS-PLAN.md stage 4.2), for one not placed + (a preview, the symbol editor) its own copy. +*/ +DiagramContext Element::elementInformations() const +{ + if (projectDataBase *store = placedStore()) + return store->elementInformation(m_uuid); + return m_data.m_informations; +} + +/** + @brief Element::placedStore + @return the project database holding this symbol's information, or + nullptr if it is not placed in a project +*/ +projectDataBase *Element::placedStore() const +{ + Diagram *d = diagram(); + QETProject *project = d ? d->project() : nullptr; + projectDataBase *store = project ? project->dataBase() : nullptr; + //A uuid other placed symbols share (F100) names no single symbol's + //information: this one answers from its own copy. + return store && store->hasElementInformation(m_uuid) + && store->placedElementCount(m_uuid) == 1 ? store : nullptr; +} + /** * @brief Element::setElementData * Set new data for this element. @@ -1860,6 +1967,7 @@ void Element::setElementData(ElementData data) if (old_info != m_data.m_informations) { m_data.m_informations.addValue(QStringLiteral("label"), actualLabel()); //Update the label if there is a formula + storeInformation(); if (diagram()) { diagram()->project()->dataBase()->elementInfoChanged(this); } @@ -2078,6 +2186,7 @@ void Element::setUpFormula(bool code_letter, QUndoCommand *parent_undo) ->elementAutoNumCurrentFormula(); m_data.m_informations.addValue(QStringLiteral("formula"), formula); + storeInformation(); QString element_currentAutoNum = diagram() ->project() @@ -2146,6 +2255,7 @@ void Element::setUpFormula(bool code_letter, QUndoCommand *parent_undo) { DiagramContext dc = m_data.m_informations; m_data.m_informations.addValue(QStringLiteral("label"), actualLabel()); + storeInformation(); emit elementInfoChange(dc, m_data.m_informations); } } @@ -2199,6 +2309,8 @@ void Element::freezeNewAddedElement() */ QString Element::actualLabel() { + //The symbol's own copy, not the store: this runs while the + //information is being changed, before the store has the change. if (m_data.m_informations.value(QStringLiteral("formula")).toString().isEmpty()) { return m_data.m_informations.value(QStringLiteral("label")).toString(); } else { diff --git a/sources/qetgraphicsitem/element.h b/sources/qetgraphicsitem/element.h index 31cddc5eb..6f5fc991d 100644 --- a/sources/qetgraphicsitem/element.h +++ b/sources/qetgraphicsitem/element.h @@ -40,6 +40,8 @@ class QUndoCommand; /** This is the base class for electrical elements. */ +class projectDataBase; + class Element : public QetGraphicsItem { friend class DiagramEventAddElement; @@ -130,8 +132,9 @@ class Element : public QetGraphicsItem } //METHODS related to information - DiagramContext elementInformations()const - {return m_data.m_informations;} + DiagramContext elementInformations()const; + /// This symbol's own copy of its information, whatever the store holds + DiagramContext ownInformations() const {return m_data.m_informations;} virtual void setElementInformations(DiagramContext dc); ElementData elementData() const; @@ -222,6 +225,9 @@ class Element : public QetGraphicsItem virtual void unlinkAllElements() {} virtual void unlinkElement(Element *) {} virtual void initLink(QETProject *); + QSet pendingLinks() const; + bool keepsSavedLinks() const {return !tmp_uuids_link.isEmpty();} + QList> waitingLinks() const; /** Resolve tmp_uuids_link against a caller-supplied candidate list instead of a project-wide search (bugtracker #607). @@ -259,8 +265,8 @@ class Element : public QetGraphicsItem */ QString linkTypeToString() const; - void newUuid() {m_uuid = QUuid::createUuid();} //create new uuid for this element - void setUuid(const QUuid &uuid) {m_uuid = uuid;} + void newUuid() {setUuid(QUuid::createUuid());} //create new uuid for this element + void setUuid(const QUuid &uuid); protected: void drawAxes(QPainter *, const QStyleOptionGraphicsItem *); @@ -329,6 +335,8 @@ class Element : public QetGraphicsItem private: void restoreSavedLinkOrder(); + void storeInformation(); + projectDataBase *placedStore() const; bool m_must_highlight = false; QSize dimensions; QPoint hotspot_coord; diff --git a/sources/qetgraphicsitem/terminal.cpp b/sources/qetgraphicsitem/terminal.cpp index 68219f11b..0e3684cea 100644 --- a/sources/qetgraphicsitem/terminal.cpp +++ b/sources/qetgraphicsitem/terminal.cpp @@ -1090,6 +1090,17 @@ QList relatedPotentialTerminal ( // If terminal parent element is a folio report. if (all_diagram && terminal -> parentElement() -> linkType() & Element::AllReport) { + //The report's partner on a folio not built yet (QET_LAZY_FOLIOS) + //is built, so that a potential is followed as far as when every + //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; + if (!waiting.isEmpty() && project + && project->state() != QETProject::ProjectParsingRunning) { + project->buildFoliosHoldingSymbols(waiting); + } QList elmt_list = terminal -> parentElement() -> linkedElements(); if (!elmt_list.isEmpty()) { diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index 70416ef25..913bc9b77 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -30,6 +30,8 @@ #include "autoNum/elementautonumschemecommand.h" #include "diagram.h" #include "qetgraphicsitem/element.h" +#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h" +#include "qetgraphicsitem/ViewItem/projectdbmodel.h" #include "qetapp.h" #include "qetmessagebox.h" #include "qetresult.h" @@ -320,7 +322,20 @@ void QETProject::init() connect(&m_titleblocks_collection, &TitleBlockTemplatesCollection::changed, this, &QETProject::updateDiagramsTitleBlockTemplate); connect(&m_titleblocks_collection, &TitleBlockTemplatesCollection::aboutToRemove, this, &QETProject::removeDiagramsTitleBlockTemplate); + //What changes the saved items of folios not built (QET_LAZY_FOLIOS): + //a folio's position, number or the folio total in their texts, + //the report and cross-reference settings, a symbol's definition. + //A folio not built is then built to be saved (writesKeptFolios()) + connect(this, &QETProject::diagramAdded, this, &QETProject::markKeptFoliosStale); + connect(this, &QETProject::diagramRemoved, this, &QETProject::markKeptFoliosStale); + connect(this, &QETProject::projectDiagramsOrderChanged, this, &QETProject::markKeptFoliosStale); + connect(this, &QETProject::reportPropertiesChanged, this, &QETProject::markKeptFoliosStale); + connect(this, &QETProject::XRefPropertiesChanged, this, &QETProject::markKeptFoliosStale); + m_undo_stack = new QUndoStack(this); + //Folios not built are saved as read until something is edited + //(m_edited_since_open, toXml()) + connect(m_undo_stack, &QUndoStack::indexChanged, this, [this]() {m_edited_since_open = true;}); connect(m_undo_stack, &QUndoStack::cleanChanged, this, &QETProject::undoStackChanged); //What counts as a change for writeBackup(): the undo stack moving, @@ -332,6 +347,7 @@ void QETProject::init() connect(&m_titleblocks_collection, &TitleBlockTemplatesCollection::aboutToRemove, this, backup_needed); connect(m_elements_collection, &XmlElementCollection::elementAdded, this, backup_needed); connect(m_elements_collection, &XmlElementCollection::elementChanged, this, backup_needed); + connect(m_elements_collection, &XmlElementCollection::elementChanged, this, &QETProject::markKeptFoliosStale); connect(m_elements_collection, &XmlElementCollection::elementRemoved, this, backup_needed); connect(m_elements_collection, &XmlElementCollection::directorieAdded, this, backup_needed); connect(m_elements_collection, &XmlElementCollection::directoryRemoved, this, backup_needed); @@ -421,6 +437,8 @@ QETProject::ProjectState QETProject::openFile(QFile *file) //Extract the content of the xml. The raw bytes are kept : a project //file without a persisted uuid derives its uuid from them. const QByteArray content = file->readAll(); + //Each folio's text is kept while it is not built (readDiagramsXml()) + m_opened_content = content; QDomDocument xml_project; // PreserveSpacingOnlyNodes: without it, a text node that is entirely // whitespace -- e.g. a title-block custom variable deliberately set to @@ -461,6 +479,7 @@ QETProject::ProjectState QETProject::openFile(QFile *file) //Build the project from the xml readProjectXml(xml_project); + m_opened_content.clear(); const qint64 total_ms = load_timer.elapsed(); qInfo().nospace().noquote() @@ -494,8 +513,12 @@ void QETProject::refresh() dlgWaiting->show(); } - for(const auto &diagram : diagrams()) + //A folio not built yet is refreshed when it is (folioLoaded()) + for(const auto &diagram : folios()) { + if (!diagram->isLoaded()) { + continue; + } if(dlgWaiting) { dlgWaiting->setProgressBar(dlgWaiting->progressBarValue()+1); @@ -520,9 +543,409 @@ QETProject::ProjectState QETProject::state() const */ QList QETProject::diagrams() const { + //Every caller of diagrams() expects every folio built: those that + //do not are moved to folios() one by one (LAZY-FOLIO-PLAN.md) + loadFolios(); return(m_diagrams_list); } +/** + @brief QETProject::folios + @return the folios of this project, whether their items are built yet + or not (a project opened with QET_LAZY_FOLIOS); see Diagram::isLoaded() +*/ +QList QETProject::folios() const +{ + return(m_diagrams_list); +} + +/** + @brief QETProject::cableLabelFolios + @return the folios CableManager::refreshLabels() works on, built: every + folio when the project has cables (a core is followed through folio + reports to any folio); without cables, the folios built and those not + built yet (QET_LAZY_FOLIOS) holding a wire that still refers to a + cable -- the only wires the pass changes then, taking the reference + away -- found in the XML kept for them +*/ +QList QETProject::cableLabelFolios() +{ + if (!cables().isEmpty()) { + return diagrams(); + } + QList referring; + for (Diagram *d : std::as_const(m_diagrams_list)) { + if (d->isLoaded()) continue; + for (const QDomElement &wire : QET::findInDomElement(d->deferredXml(), QStringLiteral("conductors"), + QStringLiteral("conductor"))) + if (!QUuid(wire.attribute(QStringLiteral("cable_uuid"))).isNull()) { + referring << d; + break; + } + } + buildFolios(referring); + return builtFolios(); +} + +/** + @brief QETProject::builtFolios + @return the folios of this project whose items are built: all of them, + but for a project opened with QET_LAZY_FOLIOS +*/ +QList QETProject::builtFolios() const +{ + QList built; + for (Diagram *d : m_diagrams_list) + if (d->isLoaded()) built << d; + return built; +} + +/** + @brief QETProject::unloadedFolioCount + @return how many folios of this project wait to be built +*/ +int QETProject::unloadedFolioCount() const +{ + int n = 0; + for (const Diagram *d : m_diagrams_list) + if (!d->isLoaded()) ++n; + return n; +} + +/** + @brief QETProject::foliosWithWiresNotReconnected + @return the folios that left out a wire of the file, an end of it not + being found (Diagram::wiresNotReconnected()). A folio not built yet is + built only when the database, filled from the document, says it has + such a wire: the others are not built to find out. +*/ +QList QETProject::foliosWithWiresNotReconnected() +{ + const QSet unjoined = m_data_base.foliosWithUnjoinedWires(); + QList found; + for (Diagram *diagram : std::as_const(m_diagrams_list)) + { + if (!diagram->isLoaded()) { + if (!unjoined.contains(diagram->uuid())) { + continue; + } + diagram->ensureLoaded(); + } + if (!diagram->wiresNotReconnected().isEmpty()) { + found << diagram; + } + } + return found; +} + +/** + @brief QETProject::loadFolios + Build every folio of this project still waiting to be built. + QET_LAZY_FOLIOS_TRACE=1 says when it happens, to find the callers that + still need every folio. +*/ +void QETProject::loadFolios() const +{ + const int waiting = unloadedFolioCount(); + if (!waiting) { + return; + } + if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS_TRACE") == 1) { + qWarning().noquote() << "lazy folios:" << waiting << "folios built for a caller of diagrams()"; + } + buildFolios(m_diagrams_list); +} + +/** + @brief QETProject::buildFolios + Build those of @p folios still waiting, as opening builds them: all + first, then what opening does after building (the cross-references of + each look for symbols on the others), with the project in the state + opening has, which keeps the texts where the file put them (a text + aligned right is not moved when its content is set). +*/ +void QETProject::buildFolios(const QList &folios) const +{ + auto *self = const_cast(this); + const ProjectState state = m_state; + self->m_state = ProjectParsingRunning; + QList built; + for (Diagram *d : folios) + if (d->buildDeferred()) built << d; + for (Diagram *d : std::as_const(built)) + self->folioLoaded(d); + //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); + //Likewise a table whose previous table was on a folio not built + for (Diagram *d : builtFolios()) + for (QGraphicsItem *item : d->items()) + if (item->type() == QetGraphicsTableItem::Type) + if (auto table = static_cast(item); + table->hasPendingLink()) + table->initLink(); + //The tables just built were filled with nothing, as opening builds + //them before the database is filled (ProjectDBModel::fillValue()): + //fill them now, as opening does then + QSet models; + for (Diagram *d : std::as_const(built)) + for (QGraphicsItem *item : d->items()) + if (item->type() == QetGraphicsTableItem::Type) + if (auto model = qobject_cast( + static_cast(item)->model())) + models.insert(model); + for (ProjectDBModel *model : std::as_const(models)) + model->folioBuilt(); + self->m_state = state; +} + +/** + @brief QETProject::buildFoliosHolding + Build the folios not built yet (QET_LAZY_FOLIOS) holding a symbol of + one of the kinds @p kinds -- if @p free_only, one which may be linked + to nothing once built: none of the partners it was saved linked to is + in the project. What a search for such symbols must build to find + every one (ElementProvider), and no more. +*/ +void QETProject::buildFoliosHolding(ElementData::Types kinds, bool free_only) +{ + if (!unloadedFolioCount()) { + return; + } + QSet unbuilt; + QSet placed; + for (Diagram *d : std::as_const(m_diagrams_list)) { + if (!d->isLoaded()) { + unbuilt.insert(d->uuid()); + } else if (free_only) { + for (Element *element : d->elements()) + placed.insert(element->uuid()); + } + } + //A symbol the database has no row for is one building leaves out + const QHash types = m_data_base.elementTypesOnFolios(unbuilt); + if (free_only) { + for (auto it = types.cbegin() ; it != types.cend() ; ++it) + placed.insert(it.key()); + } + QList to_build; + for (Diagram *d : std::as_const(m_diagrams_list)) + { + if (d->isLoaded()) { + continue; + } + for (const QDomElement &element_xml : + QET::findInDomElement(d->deferredXml(), + QStringLiteral("elements"), + QStringLiteral("element"))) + { + const auto type = types.constFind(QUuid(element_xml.attribute(QStringLiteral("uuid")))); + if (type == types.constEnd() + || !(kinds & ElementData::typeFromString(*type))) { + continue; + } + bool linked = false; + if (free_only) { + for (const QDomElement &link : + QET::findInDomElement(element_xml, + QStringLiteral("links_uuids"), + QStringLiteral("link_uuid"))) { + if (placed.contains(QUuid(link.attribute(QStringLiteral("uuid"))))) { + linked = true; + break; + } + } + } + if (!linked) { + to_build << d; + break; + } + } + } + if (to_build.isEmpty()) { + return; + } + if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS_TRACE") == 1) { + qWarning().noquote() << "lazy folios:" << to_build.size() + << "folios built for a search of" + << (free_only ? "free" : "") << "symbols of kinds" << int(kinds); + } + buildFolios(to_build); +} + +/** + @brief QETProject::buildFoliosHoldingSymbols + Build the folios not built yet (QET_LAZY_FOLIOS) holding one of the + symbols @p symbols, found in the XML kept for them: what needs those + symbols while the project is being read, before the database knows + where they are (a terminal strip's terminals) +*/ +void QETProject::buildFoliosHoldingSymbols(const QSet &symbols) +{ + if (symbols.isEmpty() || !unloadedFolioCount()) { + return; + } + QList to_build; + for (Diagram *d : std::as_const(m_diagrams_list)) + { + if (d->isLoaded()) { + continue; + } + for (const QDomElement &element_xml : + QET::findInDomElement(d->deferredXml(), + QStringLiteral("elements"), + QStringLiteral("element"))) { + if (symbols.contains(QUuid(element_xml.attribute(QStringLiteral("uuid"))))) { + to_build << d; + break; + } + } + } + if (to_build.isEmpty()) { + return; + } + if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS_TRACE") == 1) { + qWarning().noquote() << "lazy folios:" << to_build.size() + << "folios built for the symbols asked for by uuid"; + } + buildFolios(to_build); +} + +/** + @brief QETProject::symbolsOnUnbuiltFolios + @return the uuids of the symbols on the folios not built yet + (QET_LAZY_FOLIOS) that building them places +*/ +QSet QETProject::symbolsOnUnbuiltFolios() const +{ + QSet unbuilt; + for (Diagram *d : m_diagrams_list) + if (!d->isLoaded()) unbuilt.insert(d->uuid()); + QSet symbols; + if (unbuilt.isEmpty()) { + return symbols; + } + const QHash types = m_data_base.elementTypesOnFolios(unbuilt); + for (auto it = types.cbegin() ; it != types.cend() ; ++it) + symbols.insert(it.key()); + return symbols; +} + +/** + @brief QETProject::tableChainFolios + @return the folios holding a table chained to one on @p folio, before + or after it, read from the folios built and the XML kept for the + others: a chain shares its model and its columns' widths +*/ +QSet QETProject::tableChainFolios(Diagram *folio) const +{ + QHash folio_of; + QHash previous_of; + for (Diagram *d : m_diagrams_list) + { + if (d->isLoaded()) { + for (QGraphicsItem *item : d->items()) { + if (item->type() == QetGraphicsTableItem::Type) { + auto table = static_cast(item); + folio_of.insert(table->uuid(), d); + previous_of.insert(table->uuid(), table->previousTableUuid()); + } + } + continue; + } + const QDomNodeList tables = d->deferredXml().elementsByTagName( + QetGraphicsTableItem::xmlTagName()); + for (int i = 0 ; i < tables.size() ; ++i) { + const QDomElement table = tables.at(i).toElement(); + const QUuid uuid(table.attribute(QStringLiteral("uuid"))); + folio_of.insert(uuid, d); + const QDomElement previous = table.firstChildElement(QStringLiteral("previous_table")); + previous_of.insert(uuid, QUuid(previous.attribute(QStringLiteral("uuid")))); + } + } + //Every table's chain head, then the folios of the heads @p folio has + auto head = [&previous_of](QUuid uuid) { + QSet seen; + while (!previous_of.value(uuid).isNull() && !seen.contains(uuid)) { + seen.insert(uuid); + uuid = previous_of.value(uuid); + } + return uuid; + }; + QSet heads; + for (auto it = folio_of.cbegin() ; it != folio_of.cend() ; ++it) + if (it.value() == folio) + heads.insert(head(it.key())); + QSet folios; + if (heads.isEmpty()) { + return folios; + } + for (auto it = folio_of.cbegin() ; it != folio_of.cend() ; ++it) + if (heads.contains(head(it.key()))) + folios.insert(it.value()); + return folios; +} + +/** + @brief QETProject::buildFolioToShow + Build @p folio to show it, and the folios holding the partners its + symbols were saved linked to: a cross-reference or a folio report + draws what it knows of its partner, which must be built for that. + Their own partners on folios further away wait until those are built. +*/ +void QETProject::buildFolioToShow(Diagram *folio) +{ + //A folio built earlier as another's partner may still have partners + //of its own waiting: it is looked at again + if (!folio || !unloadedFolioCount()) { + return; + } + //A table chain is built at once: its model is in its first table + QSet chained = tableChainFolios(folio); + chained.insert(folio); + QList first; + for (Diagram *d : std::as_const(m_diagrams_list)) + if (chained.contains(d)) + first << d; + buildFolios(first); + QSet partners; + for (Element *elmt : folio->elements()) + partners.unite(elmt->pendingLinks()); + const QSet folio_uuids = partners.isEmpty() + ? QSet() + : m_data_base.folioUuidsOfElements(partners); + QList to_build; + for (Diagram *d : std::as_const(m_diagrams_list)) + if (!d->isLoaded() && folio_uuids.contains(d->uuid())) + to_build << d; + if (to_build.isEmpty()) { + return; + } + if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS_TRACE") == 1) { + qWarning().noquote() << "lazy folios:" << to_build.size() + << "folios built for the partners of folio" + << folio->folioIndex() + 1; + } + buildFolios(to_build); +} + +/** + @brief QETProject::folioLoaded + @p folio's items were just built, after the project was opened: do for + them what opening does for every folio's. +*/ +void QETProject::folioLoaded(Diagram *folio) +{ + linkElementsToElementAutoNums(folio); + folio->refreshContents(); +} + /** @param diagram Pointer to a Diagram object @return the folio number of the given diagram object within the project, @@ -1240,6 +1663,7 @@ QString QETProject::elementAutoNumNameClash(const QString &name, */ QVector QETProject::elementsUsingElementAutoNum(const QString &title) const { + loadFolios(); //walks the folios' symbols QVector list; const QUuid id = elementAutoNumId(title); if (id.isNull()) { @@ -1277,40 +1701,146 @@ QVector QETProject::elementsUsingElementAutoNum(const QString &title) The label is not touched: nothing it is built from changes. */ void QETProject::linkElementsToElementAutoNums() +{ + for (Diagram *d : std::as_const(m_diagrams_list)) { + if (d) linkElementsToElementAutoNums(d); + } +} + +/** + @brief QETProject::linkElementsToElementAutoNums + linkElementsToElementAutoNums() for the elements of @p folio alone +*/ +void QETProject::linkElementsToElementAutoNums(Diagram *folio) +{ + const QHash titles_by_formula = elementAutoNumTitlesByFormula(); + { + const auto items = folio->items(); + for (QGraphicsItem *it : items) { + auto *el = qgraphicsitem_cast(it); + if (!el) continue; + if (const auto id = elementAutoNumIdToLink(el->elementInformations(), + titles_by_formula)) { + el->setFormulaSchemeId(*id); + } + } + } +} + +/** + @brief QETProject::elementAutoNumTitlesByFormula + @return the titles of the element numbering schemes, by their formula +*/ +QHash QETProject::elementAutoNumTitlesByFormula() const { QHash titles_by_formula; for (auto it = m_element_autonum.constBegin(); it != m_element_autonum.constEnd(); ++it) { titles_by_formula[autonum::numerotationContextToFormula(it.value())] << it.key(); } + return titles_by_formula; +} - for (Diagram *d : std::as_const(m_diagrams_list)) { - if (!d) continue; - const auto items = d->items(); - for (QGraphicsItem *it : items) { - auto *el = qgraphicsitem_cast(it); +/** + @brief QETProject::elementAutoNumIdToLink + The rule of linkElementsToElementAutoNums() for one element, whose + information is @p information. + @return the scheme id the element must be given, or nothing when the + one it has is kept +*/ +std::optional QETProject::elementAutoNumIdToLink( + const DiagramContext &information, + const QHash &titles_by_formula) const +{ + const QString formula = information.value(QETInformation::ELMT_FORMULA).toString(); + const bool has_id = information.contains(QETInformation::ELMT_FORMULA_ID); + const QUuid id(information.value(QETInformation::ELMT_FORMULA_ID).toString()); + + if (formula.isEmpty()) { + if (has_id) return QUuid(); + return std::nullopt; + } + if (!elementAutoNumTitle(id).isEmpty()) { + return std::nullopt; + } + if (!has_id && !m_legacy_element_autonums) { + return std::nullopt; //A formula typed by hand + } + const QStringList matches = titles_by_formula.value(formula); + return matches.size() == 1 ? elementAutoNumId(matches.first()) : QUuid(); +} + +/** + @brief QETProject::symbolNumbering + @return the numbering of every symbol placed on the folios, read from + the symbol on a folio built and, on a folio not built yet + (QET_LAZY_FOLIOS), from what building it would give: its information + in the project database's store, the scheme it would be linked to, + its sequential numbers and its kind as the file has them. +*/ +QList QETProject::symbolNumbering() const +{ + QList list; + QSet unbuilt; + for (Diagram *d : m_diagrams_list) + { + if (!d->isLoaded()) { + unbuilt.insert(d->uuid()); + continue; + } + for (QGraphicsItem *item : d->items()) { + auto *el = qgraphicsitem_cast(item); if (!el) continue; const DiagramContext &info = el->elementInformations(); - const QString formula = info.value(QETInformation::ELMT_FORMULA).toString(); - const bool has_id = info.contains(QETInformation::ELMT_FORMULA_ID); - const QUuid id(info.value(QETInformation::ELMT_FORMULA_ID).toString()); - - if (formula.isEmpty()) { - if (has_id) el->setFormulaSchemeId(QUuid()); - continue; - } - if (!elementAutoNumTitle(id).isEmpty()) { - continue; - } - if (!has_id && !m_legacy_element_autonums) { - continue; //A formula typed by hand - } - const QStringList matches = titles_by_formula.value(formula); - el->setFormulaSchemeId(matches.size() == 1 - ? elementAutoNumId(matches.first()) - : QUuid()); + SymbolNumbering symbol; + symbol.element = el; + symbol.label = info.value(QETInformation::ELMT_LABEL).toString(); + symbol.formula = info.value(QETInformation::ELMT_FORMULA).toString(); + symbol.scheme = QUuid(info.value(QETInformation::ELMT_FORMULA_ID).toString()); + symbol.sequence = el->sequenceStruct(); + symbol.numbered = el->linkType() != Element::Slave + && !(el->linkType() & Element::AllReport); + list << symbol; } } + if (unbuilt.isEmpty()) { + return list; + } + + //A symbol the database has no row for is one building leaves out + //(its definition is missing) + const QHash types = m_data_base.elementTypesOnFolios(unbuilt); + const QHash titles_by_formula = elementAutoNumTitlesByFormula(); + for (Diagram *d : m_diagrams_list) + { + if (d->isLoaded()) { + continue; + } + for (const QDomElement &element_xml : + QET::findInDomElement(d->deferredXml(), + QStringLiteral("elements"), + QStringLiteral("element"))) + { + const QUuid uuid(element_xml.attribute(QStringLiteral("uuid"))); + const auto type = types.constFind(uuid); + if (type == types.constEnd()) { + continue; + } + const DiagramContext info = m_data_base.elementInformation(uuid); + SymbolNumbering symbol; + symbol.label = info.value(QETInformation::ELMT_LABEL).toString(); + symbol.formula = info.value(QETInformation::ELMT_FORMULA).toString(); + const auto linked = elementAutoNumIdToLink(info, titles_by_formula); + symbol.scheme = linked ? *linked + : QUuid(info.value(QETInformation::ELMT_FORMULA_ID).toString()); + symbol.sequence.fromXml(element_xml.firstChildElement(QStringLiteral("sequentialNumbers"))); + const ElementData::Type kind = ElementData::typeFromString(*type); + symbol.numbered = kind != ElementData::Slave + && !(kind & ElementData::AllReport); + list << symbol; + } + } + return list; } /** @@ -1451,6 +1981,7 @@ NumerotationContext QETProject::cableAutoNum(const QString &key) const @param to - last folio index to apply freeze */ void QETProject::freezeExistentConductorLabel(bool freeze, int from, int to) { + loadFolios(); //walks the folios' wires for (int i = from; i <= to; i++) { m_diagrams_list.at(i)->freezeConductors(freeze); } @@ -1494,6 +2025,7 @@ void QETProject::setFreezeNewConductors(bool set) { @param to - last folio index to apply freeze */ void QETProject::freezeExistentElementLabel(bool freeze, int from, int to) { + loadFolios(); //walks the folios' symbols for (int i = from; i <= to; i++) { m_diagrams_list.at(i)->freezeElements(freeze); } @@ -1571,7 +2103,8 @@ void QETProject::setWireHops(WireHops::Mode mode) return; } m_wire_hops = mode; - for (Diagram *diagram : diagrams()) { + //A folio not built yet reads the mode when its wires are drawn + for (Diagram *diagram : folios()) { diagram->update(); } } @@ -1598,7 +2131,8 @@ void QETProject::setUprightSymbolTexts(bool upright) return; } m_upright_symbol_texts = upright; - for (Diagram *diagram : diagrams()) { + //A folio not built yet reads the setting when its symbols are drawn + for (Diagram *diagram : folios()) { for (QGraphicsItem *item : diagram->items()) { if (Element *element = qgraphicsitem_cast(item)) { element->updateSymbolPictures(); @@ -1703,8 +2237,74 @@ void QETProject::autoFolioNumberingSelectedFolios(int from, @brief QETProject::toXml @return un document XML representant le projet */ -QDomDocument QETProject::toXml() +QDomDocument QETProject::toXml(QHash *kept_folios) { + //With @p kept_folios, a folio not built (QET_LAZY_FOLIOS) is + //written as a placeholder and its text + //as read from the file goes in kept_folios, for + //spliceKeptFolios() -- when building it would write that same + //text: the file is this version's and its database rows came from + //it (m_kept_folios_trusted), and nothing a folio's saved items are + //made from changed since: the folios' order and number, the + //project's properties, report and cross-reference settings, a + //symbol's definition (m_kept_folios_stale). Otherwise every folio is + //built and written from its items. A folio whose own data changed + //is built with the folios its links reach, as when it is shown: + //their saved texts can show its data (a report, its partner's + //folio label). + const bool keep = kept_folios && unloadedFolioCount() + && m_kept_folios_trusted && !m_kept_folios_stale; + if (keep) { + for (Diagram *diagram : std::as_const(m_diagrams_list)) + if (diagram->headerChanged()) + buildFolioToShow(diagram); + //A table filled from the database saves the size its rows + //give it, which any edit can change + if (m_edited_since_open) + for (Diagram *diagram : std::as_const(m_diagrams_list)) + if (!diagram->isLoaded() && !diagram->deferredXml().elementsByTagName( + QetGraphicsTableItem::xmlTagName()).isEmpty()) + buildFolioToShow(diagram); + //A built folio is written from its items, which need their + //partners: a symbol saves its links, a report the position of + //its partner. Build the folios of every partner still waiting, + //and of every table chain, until none is + for (int built = -1 ; built != unloadedFolioCount() ; ) + { + built = unloadedFolioCount(); + QSet waiting; + QSet chained; + for (Diagram *diagram : builtFolios()) { + for (Element *element : diagram->elements()) + waiting.unite(element->pendingLinks()); + for (Diagram *other : tableChainFolios(diagram)) + if (!other->isLoaded()) chained.insert(other); + } + buildFoliosHoldingSymbols(waiting); + QList chain_list; + for (Diagram *diagram : std::as_const(m_diagrams_list)) + if (chained.contains(diagram) && !diagram->isLoaded()) + chain_list << diagram; + buildFolios(chain_list); + } + } else { + loadFolios(); + } + //QET_CHECK_ELEMENT_INFO_STORE=1: say whether the stores of symbol + //information, folio title blocks and wire properties + //(DB-ACCESSORS-PLAN.md) agree with every placed symbol, folio and + //wire, at each save. A test and debugging aid; it changes nothing. + if (qEnvironmentVariableIntValue("QET_CHECK_ELEMENT_INFO_STORE") == 1) { + const QStringList mismatches = m_data_base.elementInformationMismatches(); + qWarning().noquote() << "element information store:" << mismatches.size() << "differ" + << mismatches.mid(0, 5).join(QStringLiteral(", ")); + const QStringList folios = m_data_base.folioTitleBlockMismatches(); + qWarning().noquote() << "folio title block store:" << folios.size() << "differ" + << folios.mid(0, 5).join(QStringLiteral(", ")); + const QStringList wires = m_data_base.conductorPropertiesMismatches(); + qWarning().noquote() << "wire properties store:" << wires.size() << "differ" + << wires.mid(0, 5).join(QStringLiteral(", ")); + } // racine du projet QDomDocument xml_doc; QDomElement project_root = xml_doc.createElement("project"); @@ -1768,6 +2368,13 @@ QDomDocument QETProject::toXml() << "[" << diagram << "]"; + if (keep && !diagram->isLoaded()) { + auto placeholder = xml_doc.createElement(QStringLiteral("diagram")); + placeholder.setAttribute(QStringLiteral("qet_kept_folio"), order_num); + kept_folios->insert(order_num ++, diagram->keptText()); + project_root.appendChild(placeholder); + continue; + } QDomElement xml_diagram = diagram->toXml().documentElement(); QDomNode xml_node = xml_doc.importNode(xml_diagram, true); @@ -1805,6 +2412,28 @@ QDomDocument QETProject::toXml() return(xml_doc); } +/** + @brief QETProject::spliceKeptFolios + @return @p text, a project written by toXml() with kept folios, with + each placeholder line replaced by the folio's text as it was read +*/ +QString QETProject::spliceKeptFolios(QString text, const QHash &kept_folios) +{ + for (auto it = kept_folios.cbegin() ; it != kept_folios.cend() ; ++it) + { + const QString placeholder = QStringLiteral("").arg(it.key()); + const qsizetype at = text.indexOf(placeholder); + if (at < 0) { + continue; + } + const qsizetype line_start = text.lastIndexOf(QLatin1Char('\n'), at) + 1; + qsizetype line_end = text.indexOf(QLatin1Char('\n'), at); + line_end = line_end < 0 ? text.size() : line_end + 1; + text.replace(line_start, line_end - line_start, QString::fromUtf8(it.value())); + } + return text; +} + /** Ferme le projet */ @@ -1864,15 +2493,22 @@ QETResult QETProject::write() return(QString("the file %1 was opened read-only and thus will not be written").arg(m_file_path)); } - QDomDocument xml_project(toXml()); QString error_message; if (isContainerPath(m_file_path)) { + // A container must contain complete folios, not lazy placeholders. + QDomDocument xml_project(toXml()); if (!writeContainer(xml_project, m_file_path, &error_message)) return(error_message); } - else if (!QET::writeXmlFile(xml_project, m_file_path, &error_message)) - return(error_message); + else + { + QHash kept_folios; + QDomDocument xml_project(toXml(&kept_folios)); + if (!QET::writeXmlText(spliceKeptFolios(xml_project.toString(4), kept_folios), + m_file_path, &error_message)) + return(error_message); + } // The project has just been written to a writable file (e.g. saved to // a new location with "Save As"), so it is no longer read-only. @@ -1927,6 +2563,7 @@ void QETProject::setReadOnly(bool read_only) */ bool QETProject::isEmpty() const { + loadFolios(); //asks the folios whether they hold anything // si le projet a un titre, on considere qu'il n'est pas vide if (!project_title_.isEmpty()) return(false); @@ -2098,12 +2735,42 @@ QString QETProject::integrateTitleBlockTemplate(const TitleBlockTemplateLocation */ bool QETProject::usesElement(const ElementsLocation &location) const { - foreach(Diagram *diagram, diagrams()) { + foreach(Diagram *diagram, builtFolios()) { if (diagram -> usesElement(location)) { return(true); } } - return(false); + return(unbuiltFolioElements().contains(location)); +} + +/** + @brief QETProject::unbuiltFolioElements + @return the locations of the symbols placed on the folios not built yet + (QET_LAZY_FOLIOS), each once, read from the XML kept for them as + building them reads it (Diagram::fromXml()) +*/ +QList QETProject::unbuiltFolioElements() const +{ + QSet types; + for (Diagram *diagram : m_diagrams_list) { + if (diagram->isLoaded()) { + continue; + } + for (const QDomElement &element_xml : + QET::findInDomElement(diagram->deferredXml(), + QStringLiteral("elements"), + QStringLiteral("element"))) { + types.insert(element_xml.attribute(QStringLiteral("type"))); + } + } + QList locations; + auto *self = const_cast(this); + for (const QString &type : std::as_const(types)) { + locations << (type.startsWith(QStringLiteral("embed://")) + ? ElementsLocation(type, self) + : ElementsLocation(type)); + } + return locations; } /** @@ -2116,10 +2783,25 @@ bool QETProject::usesElement(const ElementsLocation &location) const QList QETProject::unusedElements() const { QList unused_list; + const QList built = builtFolios(); + const QList unbuilt = unbuiltFolioElements(); foreach(ElementsLocation location, m_elements_collection->elementsLocation()) - if (location.isElement() && !usesElement(location)) + { + if (!location.isElement() || unbuilt.contains(location)) { + continue; + } + bool used = false; + for (Diagram *diagram : built) { + if (diagram->usesElement(location)) { + used = true; + break; + } + } + if (!used) { unused_list << location; + } + } return unused_list; } @@ -2133,7 +2815,8 @@ bool QETProject::usesTitleBlockTemplate(const TitleBlockTemplateLocation &locati // a diagram can only use a title block template embedded within its parent project if (location.parentProject() != this) return(false); - foreach (Diagram *diagram, diagrams()) { + //The template is the folio's own data: folios() builds nothing + foreach (Diagram *diagram, folios()) { if (diagram -> usesTitleBlockTemplate(location.name())) { return(true); } @@ -2330,6 +3013,11 @@ void QETProject::readProjectXml(QDomDocument &xml_project) //before the diagrams because the terminal strip items use it m_project_properties_handler.terminalStripLayoutHandler().fromXml(xml_project.documentElement()); + //The stores of symbol information, folio title blocks and wire + //properties are filled from the document before the folios are + //built (DB-ACCESSORS-PLAN.md stage 4.6) + m_data_base.prefillFromDocument(xml_project); + //Load the diagrams readDiagramsXml(xml_project); const qint64 diagrams_ms = phase_timer.restart(); @@ -2361,6 +3049,31 @@ void QETProject::readProjectXml(QDomDocument &xml_project) m_data_base.blockSignals(false); m_data_base.setUpdateBlocked(false); m_data_base.updateDB(xml_project); + m_data_base.endPrefill(unloadedFolioCount() > 0); + //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 + //not say which program wrote it, and one with this version's + //number can hold what a save rewrites (tst_freetextnonfinite, + //tst_foliofreezeflags): only with QET_LAZY_SAVE_KEPT=1, until a + //.qetz, whose manifest names its writer, does it + //(QETZ-DB-TRUTH-PLAN.md D4) + m_kept_folios_trusted = qEnvironmentVariableIntValue("QET_LAZY_SAVE_KEPT") == 1 + && unloadedFolioCount() > 0 + && m_project_qet_version == QetVersion::currentVersion() + && m_data_base.rowsFromDocument(); + for (Diagram *diagram : std::as_const(m_diagrams_list)) + if (!diagram->isLoaded() && diagram->keptText().isEmpty()) + m_kept_folios_trusted = false; + m_kept_folios_stale = false; + m_edited_since_open = false; + if (m_kept_folios_trusted) + for (Diagram *diagram : std::as_const(m_diagrams_list)) + diagram->rememberHeader(); + if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS_TRACE") == 1) + qWarning().noquote() << "lazy folios: opened with" << unloadedFolioCount() << "of" << m_diagrams_list.size() << "folios not built"; + if (qEnvironmentVariableIntValue("QET_CHECK_ELEMENT_INFO_STORE") == 1) + qWarning().noquote() << "filled before building:" << m_data_base.prefillReport(); const qint64 database_ms = phase_timer.elapsed(); qInfo().nospace() @@ -2418,6 +3131,38 @@ void QETProject::readDiagramsXml(QDomDocument &xml_project) if(dlgWaiting) dlgWaiting->setProgressBarRange(0, diagram_nodes.length()*3); + //Where each line of the file starts, for a folio's text: lines + //[its start, the next element's start) (QETProject::toXml()) + QList line_starts; + if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS") == 1 && !m_opened_content.isEmpty()) { + line_starts << 0; + for (qsizetype i = 0 ; i < m_opened_content.size() ; ++i) + if (m_opened_content.at(i) == '\n') line_starts << i + 1; + } + auto folio_text = [this, &line_starts](const QDomElement &folio) { + const int first = folio.lineNumber(); + const QDomElement next = folio.nextSiblingElement(); + int last = next.isNull() ? -1 : next.lineNumber(); + if (last < 0) { + //The last folio: up to the project's closing tag + const qsizetype end = m_opened_content.lastIndexOf(""); + last = end < 0 ? -1 : int(std::upper_bound(line_starts.cbegin(), line_starts.cend(), end) + - line_starts.cbegin()); + } + if (first < 1 || last <= first || last > line_starts.size()) { + return QByteArray(); + } + const QByteArray text = m_opened_content.mid(line_starts.at(first - 1), + line_starts.at(last - 1) - line_starts.at(first - 1)); + //One folio, whole: else it is built to be saved + const QByteArray trimmed = text.trimmed(); + if (!trimmed.startsWith("") || (trimmed.endsWith("/>") && !text.contains("")))) { + return QByteArray(); + } + return text; + }; + for (int i = 0 ; i < diagram_nodes.length() ; ++ i) { if(dlgWaiting) @@ -2436,7 +3181,12 @@ void QETProject::readDiagramsXml(QDomDocument &xml_project) connect(diagram, &Diagram::usedTitleBlockTemplateChanged, this, &QETProject::usedTitleBlockTemplateChanged); + m_deferring_folios = qEnvironmentVariableIntValue("QET_LAZY_FOLIOS") == 1; diagram->initFromXml(diagram_xml_element); + m_deferring_folios = false; + if (!diagram->isLoaded() && !line_starts.isEmpty()) + diagram->setKeptText(folio_text(diagram_xml_element)); + m_data_base.placeFolio(diagram); if(dlgWaiting) dlgWaiting->setDetail(diagram->title()); } @@ -3072,11 +3822,13 @@ void QETProject::writeBackup() //Capture the document by value (implicitly shared, so cheap): the //Qt5-style QtConcurrent::run(function, reference-args) call did not //survive the Qt6 API change, a lambda behaves identically on both. - QDomDocument xml_project(toXml()); + QHash kept_folios; + QDomDocument xml_project(toXml(&kept_folios)); KAutoSaveFile *target = &m_backup_files[m_next_backup_slot]; m_next_backup_slot = (m_next_backup_slot + 1) % BackupGenerations; - m_backup_future = QtConcurrent::run([target, xml_project]() mutable { - return QET::writeToFile(xml_project, target, nullptr); + m_backup_future = QtConcurrent::run([target, xml_project, kept_folios]() mutable { + return QET::writeTextToFile(spliceKeptFolios(xml_project.toString(4), kept_folios), + target, nullptr); }); } @@ -3104,6 +3856,8 @@ DiagramContext QETProject::projectProperties() */ void QETProject::setProjectProperties(const DiagramContext &context) { m_project_properties = context; + //Folio and symbol texts are made from them + markKeptFoliosStale(); updateDiagramsFolioData(); emit projectInformationsChanged(this); } diff --git a/sources/qetproject.h b/sources/qetproject.h index 58dd124c6..30da9d1a4 100644 --- a/sources/qetproject.h +++ b/sources/qetproject.h @@ -31,6 +31,7 @@ #include "wirehops.h" #include "wiringrules.h" #include "diagram.h" +#include "autoNum/assignvariables.h" #ifdef BUILD_WITHOUT_KF # include "ui/nokde/kautosavefile.h" #else @@ -44,6 +45,7 @@ #include #include +#include class Diagram; class Element; @@ -119,6 +121,22 @@ class QETProject : public QObject QUuid derivedItemUuid(const QString &kind, const QString &key); ProjectState state() const; QList diagrams() const; + //The folios whether their items are built or not + //(QET_LAZY_FOLIOS, LAZY-FOLIO-PLAN.md); diagrams() builds them + QList folios() const; + bool deferringFolios() const {return m_deferring_folios;} + int unloadedFolioCount() const; + QList builtFolios() const; + QList cableLabelFolios(); + QList foliosWithWiresNotReconnected(); + void buildFolioToShow(Diagram *folio); + void buildFoliosHolding(ElementData::Types kinds, bool free_only); + void buildFoliosHoldingSymbols(const QSet &symbols); + QSet symbolsOnUnbuiltFolios() const; + QSet tableChainFolios(Diagram *folio) const; + void loadFolios() const; + void buildFolios(const QList &folios) const; + void folioLoaded(Diagram *folio); int folioIndex(const Diagram *) const; XmlElementCollection *embeddedElementCollection()const; TitleBlockTemplatesProjectCollection *embeddedTitleBlockTemplatesCollection(); @@ -240,6 +258,18 @@ class QETProject : public QObject const QString &ignored_title = QString()) const; static QString normalizedAutoNumName(const QString &name); QVector elementsUsingElementAutoNum(const QString &title) const; + /// A placed symbol's numbering, whether its folio is built or + /// not (QET_LAZY_FOLIOS): what is asked of every symbol of the + /// project when one is numbered + struct SymbolNumbering { + const Element *element = nullptr; ///< null on a folio not built + QString label; + QString formula; + QUuid scheme; ///< the element numbering scheme it follows + autonum::sequentialNumbers sequence; + bool numbered = false; ///< neither a slave nor a report + }; + QList symbolNumbering() const; /** * @brief Renumber existing elements by element autonumbering scheme. @@ -283,7 +313,8 @@ class QETProject : public QObject void autoFolioNumberingNewFolios (); void autoFolioNumberingSelectedFolios(int, int, const QString&); - QDomDocument toXml(); + QDomDocument toXml(QHash *kept_folios = nullptr); + static QString spliceKeptFolios(QString text, const QHash &kept_folios); bool close(); QETResult write(); bool isReadOnly() const; @@ -293,6 +324,7 @@ class QETProject : public QObject QString integrateTitleBlockTemplate(const TitleBlockTemplateLocation &, MoveTitleBlockTemplatesHandler *handler); bool usesElement(const ElementsLocation &) const; QList unusedElements() const; + QList unbuiltFolioElements() const; bool usesTitleBlockTemplate(const TitleBlockTemplateLocation &); bool projectWasModified(); bool projectOptionsWereModified(); @@ -474,6 +506,24 @@ class QETProject : public QObject /// saved without an id (written before ids existed) bool m_legacy_element_autonums = false; void linkElementsToElementAutoNums(); + void linkElementsToElementAutoNums(Diagram *folio); + QHash elementAutoNumTitlesByFormula() const; + std::optional elementAutoNumIdToLink( + const DiagramContext &information, + const QHash &titles_by_formula) const; + bool m_deferring_folios = false; + /// Saving may write a folio not built as it was read + /// (writesKeptFolios()): the file is this version's, its + /// database rows came from it, and nothing project-wide that + /// a folio's saved items depend on has changed since + bool m_kept_folios_trusted = false; + bool m_kept_folios_stale = false; + /// Something was edited since the project was opened: a table + /// filled from the database may have changed + bool m_edited_since_open = false; + void markKeptFoliosStale() {m_kept_folios_stale = true;} + /// The file being read, for each folio's text (readDiagramsXml()) + QByteArray m_opened_content; bool m_auto_conductor = true; WireHops::Mode m_wire_hops = WireHops::Mode::None; /// Texts drawn in a turned symbol stay horizontal (on for a new diff --git a/sources/ui/diagrambgcolorbutton.cpp b/sources/ui/diagrambgcolorbutton.cpp index 037b8bc43..20097e037 100644 --- a/sources/ui/diagrambgcolorbutton.cpp +++ b/sources/ui/diagrambgcolorbutton.cpp @@ -150,8 +150,9 @@ void DiagramBgColorToolButton::applyColor(const QColor &color) if (!editor) { return; } + //A folio not built yet takes the colour when it is drawn for (ProjectView *pv : editor->openedProjects()) - for (Diagram *d : pv->project()->diagrams()) + for (Diagram *d : pv->project()->folios()) d->update(); } @@ -175,8 +176,9 @@ void DiagramBgColorToolButton::applySystemColor() if (!editor) { return; } + //A folio not built yet takes the colour when it is drawn for (ProjectView *pv : editor->openedProjects()) - for (Diagram *d : pv->project()->diagrams()) + for (Diagram *d : pv->project()->folios()) d->update(); } diff --git a/sources/ui/diagrameditorhandlersizewidget.cpp b/sources/ui/diagrameditorhandlersizewidget.cpp index bb22cac90..a0726e4aa 100644 --- a/sources/ui/diagrameditorhandlersizewidget.cpp +++ b/sources/ui/diagrameditorhandlersizewidget.cpp @@ -60,7 +60,8 @@ void DiagramEditorHandlerSizeWidget::on_comboBox_currentIndexChanged(int index) { editor_->setProperty("graphics_handler_size", size); for (auto project_view : editor_->openedProjects()) { - for (auto diagram : project_view->project()->diagrams()) { + //Handles exist on built folios only + for (auto diagram : project_view->project()->builtFolios()) { for (const auto item : diagram->items()) { if (item->type() == QetGraphicsHandlerItem::Type) { auto handler = qgraphicsitem_cast(item); diff --git a/sources/ui/jumptoelementdialog.cpp b/sources/ui/jumptoelementdialog.cpp index a2bf35089..eaaaaf2eb 100644 --- a/sources/ui/jumptoelementdialog.cpp +++ b/sources/ui/jumptoelementdialog.cpp @@ -169,7 +169,7 @@ void JumpToElementDialog::activateCurrentItem() const int index = current->data(Qt::UserRole).toInt(); if (index == -1) { const QList diagrams = m_diagram->project() - ? m_diagram->project()->diagrams() + ? m_diagram->project()->folios() : QList(); const int folio = current->data(Qt::UserRole + 2).toInt(); if (folio >= 0 && folio < diagrams.size()) { @@ -208,7 +208,8 @@ QListWidgetItem *JumpToElementDialog::cellItem(const QString &needle) const if (!m_diagram || !m_diagram->project()) { return nullptr; } - const QList diagrams = m_diagram->project()->diagrams(); + //Folios' cells only (their border): folios() builds nothing + const QList diagrams = m_diagram->project()->folios(); Diagram *diagram = m_diagram; QString cell = needle; diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index faa763b3a..b5be0cf96 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -87,6 +87,13 @@ if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja") --source ${CMAKE_CURRENT_SOURCE_DIR}/attached_plain_paste_probe.cpp --fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet) set_tests_properties(tst_attachedplainpaste PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE) + add_test(NAME tst_lazyfolios + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_lazy_folios_probe.py + --build ${CMAKE_BINARY_DIR} --binary $ + --source ${CMAKE_CURRENT_SOURCE_DIR}/lazy_folios_probe.cpp + --fixture ${CMAKE_CURRENT_SOURCE_DIR}/../../examples/industrial.qet) + set_tests_properties(tst_lazyfolios PROPERTIES TIMEOUT 600 RUN_SERIAL TRUE + ENVIRONMENT "QT_QPA_PLATFORM=offscreen") # Deleting a cable, undoing the delete twice and then editing anything # has both cable commands tearing down the same cable. Runs under the # same objects the application uses, so a second free is caught. diff --git a/tests/qttest/lazy_folios_probe.cpp b/tests/qttest/lazy_folios_probe.cpp new file mode 100644 index 000000000..daebbba50 --- /dev/null +++ b/tests/qttest/lazy_folios_probe.cpp @@ -0,0 +1,579 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Lazy folios (QET_LAZY_FOLIOS, LAZY-FOLIO-PLAN.md stages 5.2 and 5.3), +// linked against the application's objects: a folio shown in a project +// opened without building its folios draws and links as in the project +// opened as usual, building only it and the folios its links and tables +// reach, and the project saves the same. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../../sources/diagram.h" +#include "../../sources/qetproject.h" +#include "../../sources/qetresult.h" +#include "../../sources/qetmessagebox.h" +#include "../../sources/qetgraphicsitem/element.h" +#include "../../sources/autoNum/elementautonumschemecommand.h" +#include "../../sources/elementprovider.h" +#include "../../sources/TerminalStrip/terminalstrip.h" +#include "../../sources/ui/elementpropertieswidget.h" +#include "../../sources/conductorautonumerotation.h" +#include "../../sources/diagramcontent.h" +#include "../../sources/diagramview.h" +#include "../../sources/diagramevent/diagrameventaddpaste.h" +#include +#include "../../sources/qetgraphicsitem/conductor.h" +#include "../../sources/qetgraphicsitem/terminal.h" +#include "../../sources/undocommand/changeelementinformationcommand.h" +#include "../../sources/undocommand/changetitleblockcommand.h" +#include "../../sources/undocommand/deleteqgraphicsitemcommand.h" +#include "../../sources/factory/elementfactory.h" +#include "../../sources/undocommand/addgraphicsobjectcommand.h" + +static void check(bool value, const char *message) { if (!value) throw std::runtime_error(message); } + + //Every symbol's numbering, in a stable order +static QStringList numbering(const QETProject &project) +{ + QStringList list; + for (const auto &symbol : project.symbolNumbering()) + list << QStringList{symbol.label, symbol.formula, symbol.scheme.toString(), + symbol.sequence.unit.join(';'), symbol.sequence.ten.join(';'), + symbol.sequence.hundred.join(';'), + QString::number(symbol.numbered)}.join('|'); + list.sort(); + return list; +} + + //What the scheme @p title tells of the numbers its symbols carry +static QString gaps(const QETProject &project, const QString &title) +{ + QStringList list; + for (const auto &gap : ElementAutoNumSchemeCommand::gapRanges(&project, title)) + list << QString("%1-%2").arg(gap.from).arg(gap.to); + return list.join(','); +} + + //A symbol of @p location placed on folio @p i as the folio's editor + //places one (DiagramEventAddElement::addElement()), numbered with the + //scheme @p title: its label, and the numbers then free for it +static QString place(QETProject &project, int i, const QString &location, const QString &title) +{ + Diagram *folio = project.folios().at(i); + project.buildFolioToShow(folio); + project.setCurrrentElementAutonum(title); + int state = 0; + Element *element = ElementFactory::Instance()->createElement( + ElementsLocation(location, &project), nullptr, &state); + check(!state, "symbol made"); + element->setPos(QPointF(-500, -500)); + folio->addItem(element); + auto *undo = new QUndoCommand("insert"); + new AddGraphicsObjectCommand(element, folio, element->pos(), undo); + element->setUpFormula(true, undo); + folio->undoStack().push(undo); + element->freezeNewAddedElement(); + QStringList free; + for (int n : ElementAutoNumSchemeCommand::freeNumbers(&project, title, element)) + free << QString::number(n); + return element->actualLabel() + " free " + free.join(','); +} + + //The symbols a search of a project finds, by uuid +static QStringList found(const QVector> &elements) +{ + QStringList list; + for (const auto &element : elements) + list << element->uuid().toString(); + list.sort(); + return list; +} + + //Everyday edits on folio @p i, as its editor makes them: a symbol's + //information, a wire, the folio's title block, a linked symbol + //deleted, then one undo and one redo +static void edit(QETProject &project, int i) +{ + Diagram *folio = project.folios().at(i); + project.buildFolioToShow(folio); + QList elements = folio->elements(); + std::sort(elements.begin(), elements.end(), [](Element *a, Element *b) { + return a->uuid() < b->uuid(); }); + check(elements.size() > 2, "a folio with symbols to edit"); + + const DiagramContext old_info = elements.first()->elementInformations(); + DiagramContext new_info = old_info; + new_info.addValue(QStringLiteral("comment"), QStringLiteral("edited lazily")); + folio->undoStack().push(new ChangeElementInformationCommand(elements.first(), old_info, new_info)); + + Terminal *ends[2] = {nullptr, nullptr}; + for (Element *element : elements) { + for (Terminal *terminal : element->terminals()) { + if (terminal->conductors().isEmpty() + && (!ends[0] || ends[0]->parentElement() != element)) { + ends[ends[0] ? 1 : 0] = terminal; + break; + } + } + if (ends[1]) break; + } + if (ends[1]) { + auto *undo = new QUndoCommand(QStringLiteral("wire")); + auto *wire = new Conductor(ends[0], ends[1]); + new AddGraphicsObjectCommand(wire, folio, QPointF(), undo); + ConductorAutoNumerotation numbering(wire, folio, undo); + numbering.numerate(); + folio->undoStack().push(undo); + } + + const TitleBlockProperties old_block = folio->border_and_titleblock.exportTitleBlock(); + TitleBlockProperties new_block = old_block; + new_block.title += QStringLiteral(" (edited)"); + folio->undoStack().push(new ChangeTitleBlockCommand(folio, old_block, new_block)); + + //Copy three symbols and paste them, as Ctrl+C then Ctrl+Shift+V + //and Return do + { + DiagramView view(folio); + folio->clearSelection(); + for (int n = 0 ; n < 3 ; ++n) + elements.at(n)->setSelected(true); + view.copy(); + auto *paste = new DiagramEventAddPaste(folio, QPointF(), DiagramEventAddPaste::AtOrigin); + folio->setEventInterface(paste); + QKeyEvent drop(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier); + paste->keyPressEvent(&drop); + folio->clearEventInterface(); + } + + Element *linked = nullptr; + for (Element *element : elements) + if (!element->linkedElements().isEmpty()) { linked = element; break; } + folio->clearSelection(); + (linked ? linked : elements.last())->setSelected(true); + folio->undoStack().push(new DeleteQGraphicsItemCommand(folio, DiagramContent(folio, true))); + + project.undoStack()->undo(); + project.undoStack()->redo(); +} + + //The project as saving writes it: folios not built written as read + //when building them would write the same (QETProject::toXml()) +static QString saved(QETProject &project) +{ + QHash kept; + const QDomDocument document = project.toXml(&kept); + return QETProject::spliceKeptFolios(document.toString(4), kept); +} + + //The rows of the project database's tables, in their order, read as + //every caller reads them (newQuery(): brought up to date first) +static QStringList tables(QETProject &project) +{ + QStringList rows; + for (const char *table : {"diagram", "diagram_info", "element", "element_info", + "terminal", "conductor", "link"}) { + QSqlQuery query = project.dataBase()->newQuery( + QStringLiteral("SELECT * FROM %1 ORDER BY rowid").arg(QLatin1String(table))); + check(query.exec(), "a table read"); + QStringList table_rows; + while (query.next()) { + QStringList row; + for (int i = 0 ; i < query.record().count() ; ++i) + row << query.value(i).toString(); + table_rows << QLatin1String(table) + ": " + row.join('|'); + } + //Rows are written in the order the scene gives a folio's items, + //which is not the same from one opening to the next: two usual + //opens given the same edits differ in it (637 rows of 3 394 on + //industrial.qet). What a table holds is compared, not its order + table_rows.sort(); + rows << table_rows; + } + return rows; +} + +static QImage render(Diagram *folio) +{ + const QRectF rect = folio->border_and_titleblock.borderAndTitleBlockRect().adjusted(-5, -5, 5, 5); + QImage image(rect.size().toSize(), QImage::Format_RGB32); + image.fill(Qt::white); + QPainter painter(&image); + folio->render(&painter, QRectF(QPointF(0, 0), rect.size()), rect); + return image; +} + + //Each symbol's partners, in their order +static QStringList links(Diagram *folio) +{ + QStringList list; + for (Element *element : folio->elements()) { + QStringList partners; + for (Element *partner : element->linkedElements()) + partners << partner->uuid().toString(); + list << element->uuid().toString() + ' ' + partners.join(','); + } + list.sort(); + return list; +} + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); std::freopen(argv[2], "w", stdout); + QTemporaryDir settings; QSettings::setDefaultFormat(QSettings::IniFormat); + QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, settings.path()); + QCoreApplication::setOrganizationName("QETLazyFoliosRegression"); + QETProject::setBackupEnabled(false); QET::QetMessageBox::setNonInteractive(true); + QFontDatabase::addApplicationFont(":/fonts/LiberationSans-Regular.ttf"); + //Folios not built saved as read (QETProject::toXml()): this + //fixture is written by this version's own save + qputenv("QET_LAZY_SAVE_KEPT", "1"); + try { + const QString output = QString::fromLocal8Bit(argv[3]); + qunsetenv("QET_LAZY_FOLIOS"); + //A file saved by this version: its folios carry their uuids, + //which opening without building them needs + const QString path = output + "/lazy_folios.qet"; + QSet strip_folios; + { + QETProject source(QString::fromLocal8Bit(argv[1])); + check(source.state() == QETProject::Ok, "fixture opens"); + //A terminal strip of terminals on several folios: it is + //read with the project, before any folio is built + TerminalStrip *strip = source.newTerminalStrip(QStringLiteral("=A1"), QStringLiteral("+B1"), + QStringLiteral("X99")); + int added = 0; + for (Diagram *folio : source.folios()) + for (Element *element : folio->elements()) + if (element->elementData().m_type == ElementData::Terminal && added < 12 + && strip->addTerminal(element)) { + ++added; + strip_folios.insert(source.folioIndex(folio)); + } + check(added > 3, "a terminal strip with terminals"); + source.setFilePath(path); + check(source.write().isOk(), "fixture saved by this version"); + } + QETProject eager(path); + check(eager.state() == QETProject::Ok, "project opens"); + { + qputenv("QET_LAZY_FOLIOS", "1"); + QETProject lazy_strips(path); + qunsetenv("QET_LAZY_FOLIOS"); + auto strip_of = [](const QETProject &project) { + for (TerminalStrip *strip : project.terminalStrip()) + if (strip->name() == QStringLiteral("X99")) return strip->physicalTerminalCount(); + return -1; + }; + check(strip_of(lazy_strips) > 3 && strip_of(lazy_strips) == strip_of(eager), + "a terminal strip keeps its terminals when the folios are not built"); + } + const QList folios = eager.folios(); + check(folios.size() > 3, "several folios"); + QList images; QList partners; + int linked = 0; + for (Diagram *folio : folios) { + images << render(folio); + partners << links(folio); + for (Element *element : folio->elements()) + linked += !element->linkedElements().isEmpty(); + } + check(linked > 0, "the project has linked symbols"); + + qputenv("QET_LAZY_FOLIOS", "1"); + auto same = [&](QETProject &lazy, int i, const char *what) { + Diagram *folio = lazy.folios().at(i); + lazy.buildFolioToShow(folio); + if (render(folio) != images.at(i) || links(folio) != partners.at(i)) { + std::printf("folio %d, %s: differs\n", i + 1, what); + throw std::runtime_error("a folio shown lazily draws or links as opened as usual"); + } + }; + //Each folio first after opening + for (int i = 0; i < folios.size(); ++i) { + QETProject lazy(path); + check(lazy.unloadedFolioCount() == folios.size() - strip_folios.size(), + "opened building only the folios of the terminal strip's terminals"); + same(lazy, i, "shown first"); + check(lazy.unloadedFolioCount() > 0, "showing a folio does not build them all"); + } + //Every folio, one after the other, then the save + QETProject lazy(path); + for (int i = 0; i < folios.size(); ++i) + same(lazy, i, "shown in turn"); + check(lazy.toXml().toString() == eager.toXml().toString(), "saves as opened as usual"); + check(lazy.undoStack()->isClean(), "showing folios changes nothing"); + + //Numbering asks every symbol of the project: it is answered for + //the folios not built without building them, as when built + QETProject numbered(path); + check(numbering(numbered) == numbering(eager), "symbols' numbering as when built"); + const QStringList titles = eager.elementAutoNum().keys(); + check(!titles.isEmpty(), "the project has element numbering schemes"); + for (const QString &title : titles) + check(gaps(numbered, title) == gaps(eager, title), "a scheme's gaps as when built"); + check(numbered.unloadedFolioCount() == folios.size() - strip_folios.size(), "numbering read without building"); + //A symbol placed on a folio, numbered by a scheme others follow + QString location, title; + int folio = -1; + for (const auto &symbol : eager.symbolNumbering()) { + if (symbol.numbered && symbol.element && !eager.elementAutoNumTitle(symbol.scheme).isEmpty()) { + location = symbol.element->location().toString(); + title = eager.elementAutoNumTitle(symbol.scheme); + folio = eager.folios().indexOf(symbol.element->diagram()); + break; + } + } + check(folio >= 0, "a symbol following a scheme"); + const QString placed = place(numbered, folio, location, title); + std::printf("placed on folio %d with %s: %s\n", folio + 1, qPrintable(title), qPrintable(placed)); + check(numbered.unloadedFolioCount() > 0, "numbering a placed symbol does not build every folio"); + QETProject usual(path); + check(placed == place(usual, folio, location, title), "a placed symbol numbered as when built"); + //The candidates a link tab lists (ElementProvider): found as + //when built, building only the folios which hold some + const ElementProvider usual_search(&eager); + for (const ElementData::Types kinds : {ElementData::Types(ElementData::Slave), + ElementData::Types(ElementData::Master), + ElementData::Types(ElementData::AllReport), + ElementData::Types(ElementData::Terminal)}) { + QETProject searched(path); + const ElementProvider search(&searched); + const QStringList lazy_free = found(search.freeElement(kinds)), usual_free = found(usual_search.freeElement(kinds)); + if (lazy_free != usual_free) { + for (const QString &u : usual_free) if (!lazy_free.contains(u)) std::printf(" only when built: %s\n", qPrintable(u)); + for (const QString &u : lazy_free) if (!usual_free.contains(u)) std::printf(" only lazily: %s\n", qPrintable(u)); + } + check(lazy_free == usual_free, "free symbols found as when built"); + std::printf("free symbols of kinds %d: %d folios built\n", int(kinds), + int(folios.size() - searched.unloadedFolioCount())); + check(found(search.find(kinds)) == found(usual_search.find(kinds)), + "symbols found as when built"); + } + { + QETProject searched(path); + ElementProvider(&searched).freeElement(ElementData::Slave); + check(searched.unloadedFolioCount() > 0, "a search for free contacts does not build every folio"); + } + //Selecting a symbol shows its properties, a link tab for a + //coil, a contact or a report: the folio and few others built + for (const Element::kind kind : {Element::Master, Element::Slave, Element::NextReport, + Element::PreviousReport}) { + QETProject selected(path); + Element *element = nullptr; + for (int i = 0 ; !element && i < folios.size() ; ++i) { + for (Element *candidate : eager.folios().at(i)->elements()) { + if (candidate->linkType() == kind) { + selected.buildFolioToShow(selected.folios().at(i)); + for (Element *e : selected.folios().at(i)->elements()) + if (e->uuid() == candidate->uuid()) element = e; + break; + } + } + } + check(element, "a symbol of each kind with a link tab"); + const int before = selected.unloadedFolioCount(); + delete new ElementPropertiesWidget(element); + std::printf("properties of a symbol of kind %d: %d more folios built, %d not built\n", + int(kind), before - selected.unloadedFolioCount(), selected.unloadedFolioCount()); + check(selected.unloadedFolioCount() > 0, "selecting a symbol does not build every folio"); + } + //Everyday edits on a folio linked to others: the others stay + //not built, and the project saves as when they are + int busiest = 0, most = -1; + for (int i = 0 ; i < folios.size() ; ++i) { + int partners = 0; + for (Element *element : folios.at(i)->elements()) + for (Element *partner : element->linkedElements()) + partners += partner->diagram() != folios.at(i); + if (partners > most) { most = partners; busiest = i; } + } + { + QETProject edited(path), usual(path); + qunsetenv("QET_LAZY_FOLIOS"); + QETProject usual_edited(path); + qputenv("QET_LAZY_FOLIOS", "1"); + edit(edited, busiest); + std::printf("edits on folio %d (%d links to other folios): %d folios not built\n", + busiest + 1, most, edited.unloadedFolioCount()); + check(edited.unloadedFolioCount() > 0, "editing a folio does not build every folio"); + edit(usual_edited, busiest); + //Made by the edits, a new wire's uuid is random: any uuid the + //file had not is "new" before comparing + QFile saved(path); saved.open(QIODevice::ReadOnly); + const QString before = QString::fromUtf8(saved.readAll()); + static const QRegularExpression uuid_re(QStringLiteral("uuid=\"(\\{[0-9a-f-]{36}\\})\"")); + auto known = [&before](QString xml) { + QString out; + qsizetype at = 0; + for (auto it = uuid_re.globalMatch(xml) ; it.hasNext() ; ) { + const auto match = it.next(); + out += xml.mid(at, match.capturedStart() - at); + out += before.contains(match.captured(1)) ? match.captured(0) + : QStringLiteral("uuid=\"new\""); + at = match.capturedEnd(); + } + return out + xml.mid(at); + }; + const QStringList lazy_rows = tables(edited), usual_rows = tables(usual_edited); + static const QRegularExpression bare_uuid_re(QStringLiteral("\\{[0-9a-f-]{36}\\}")); + auto known_row = [&before](QString row) { + for (auto it = bare_uuid_re.globalMatch(QString(row)) ; it.hasNext() ; ) { + const QString uuid = it.next().captured(0); + if (!before.contains(uuid)) row.replace(uuid, QStringLiteral("new")); + } + return row; + }; + check(edited.unloadedFolioCount() > 0, "reading the database does not build every folio"); + QStringList lazy_known, usual_known; + for (const QString &row : lazy_rows) lazy_known << known_row(row); + for (const QString &row : usual_rows) usual_known << known_row(row); + lazy_known.sort(); + usual_known.sort(); + int differing = 0; + for (int i = 0 ; i < std::max(lazy_known.size(), usual_known.size()) ; ++i) + if (lazy_known.value(i) != usual_known.value(i) && differing++ < 5) + std::printf(" row %d: lazy %s\n usual %s\n", i, qPrintable(lazy_known.value(i).left(160)), + qPrintable(usual_known.value(i).left(160))); + std::printf("database after the edits: %d rows, %d differ\n", int(usual_rows.size()), differing); + check(!differing, "the database after the edits as when every folio is built"); + //A pasted copy has a random uuid, and a folio's symbols are + //saved in uuid order: the lines are compared, not their order + auto lines = [&known](const QString &xml) { + QStringList list = known(xml).split(QLatin1Char('\n')); + list.sort(); + return list.join(QLatin1Char('\n')); + }; + const QString a = lines(edited.toXml().toString()), b = lines(usual_edited.toXml().toString()); + if (a != b) { + QFile fa(output + "/edited-lazy.xml"); fa.open(QIODevice::WriteOnly); fa.write(a.toUtf8()); + QFile fb(output + "/edited-usual.xml"); fb.open(QIODevice::WriteOnly); fb.write(b.toUtf8()); + } + check(a == b, + "edits save as when every folio is built"); + } + //Saving writes the folios not built as they were read, the same + //bytes as building them would write + const QString usual_text = saved(eager); + { + QETProject untouched(path); + const int unbuilt = untouched.unloadedFolioCount(); + const QString untouched_text = saved(untouched); + if (untouched_text != usual_text) { + QFile fa(output + "/untouched-lazy.xml"); fa.open(QIODevice::WriteOnly); fa.write(untouched_text.toUtf8()); + QFile fb(output + "/untouched-usual.xml"); fb.open(QIODevice::WriteOnly); fb.write(usual_text.toUtf8()); + } + check(untouched_text == usual_text, "a project saved without building, as when built"); + //Only the partners of the folios the terminal strip built + std::printf("saved untouched: %d of %d folios not built built to save\n", + unbuilt - untouched.unloadedFolioCount(), unbuilt); + check(untouched.unloadedFolioCount() > 0, "saving does not build every folio"); + } + //After everyday edits on a folio: as when built, others not built + { + QETProject edited(path), usual(path); + qunsetenv("QET_LAZY_FOLIOS"); + QETProject usual_edited(path); + qputenv("QET_LAZY_FOLIOS", "1"); + edit(edited, busiest); + edit(usual_edited, busiest); + QFile file(path); file.open(QIODevice::ReadOnly); + const QString before = QString::fromUtf8(file.readAll()); + static const QRegularExpression uuid_re(QStringLiteral("\\{[0-9a-f-]{36}\\}")); + auto lines = [&before](QString text) { + for (auto it = uuid_re.globalMatch(QString(text)) ; it.hasNext() ; ) { + const QString uuid = it.next().captured(0); + if (!before.contains(uuid)) text.replace(uuid, QStringLiteral("new")); + } + QStringList list = text.split(QLatin1Char('\n')); + list.sort(); + return list; + }; + const QStringList la = lines(saved(edited)), lb = lines(saved(usual_edited)); + if (la != lb) { + int shown = 0; + for (const QString &l : lb) if (!la.contains(l) && shown++ < 6) std::printf(" only usual: %s\n", qPrintable(l.trimmed().left(150))); + shown = 0; + for (const QString &l : la) if (!lb.contains(l) && shown++ < 6) std::printf(" only lazy: %s\n", qPrintable(l.trimmed().left(150))); + } + check(la == lb, "edits saved without building, as when built"); + std::printf("saved after the edits: %d folios not built\n", edited.unloadedFolioCount()); + check(edited.unloadedFolioCount() > 0, "saving after edits does not build every folio"); + } + //A folio not built given a new title: it and the folios its + //links reach are built to be saved, the others not + { + QETProject renamed(path); + qunsetenv("QET_LAZY_FOLIOS"); + QETProject usual_renamed(path); + qputenv("QET_LAZY_FOLIOS", "1"); + for (QETProject *project : {&renamed, &usual_renamed}) { + Diagram *folio = project->folios().at(busiest); + TitleBlockProperties block = folio->border_and_titleblock.exportTitleBlock(); + block.title += QStringLiteral(" (renamed)"); + folio->border_and_titleblock.importTitleBlock(block); + } + check(saved(renamed) == saved(usual_renamed), "a renamed folio saved as when built"); + std::printf("saved after a folio not built was renamed: %d folios not built\n", renamed.unloadedFolioCount()); + check(renamed.unloadedFolioCount() > 0, "renaming a folio does not build every folio to save"); + } + //Project properties, which any folio's texts can use: every + //folio is built to be saved + { + QETProject changed(path); + qunsetenv("QET_LAZY_FOLIOS"); + QETProject usual_changed(path); + qputenv("QET_LAZY_FOLIOS", "1"); + for (QETProject *project : {&changed, &usual_changed}) { + DiagramContext properties = project->projectProperties(); + properties.addValue(QStringLiteral("lazy_test"), QStringLiteral("1")); + project->setProjectProperties(properties); + } + check(saved(changed) == saved(usual_changed), "changed project properties saved as when built"); + check(changed.unloadedFolioCount() == 0, "changed project properties build every folio to save"); + } + //A potential through folio reports is followed as far as when + //every folio is built + { + Conductor *usual_wire = nullptr; + for (Diagram *folio : eager.folios()) + for (Element *element : folio->elements()) + if ((element->linkType() & Element::AllReport) && !element->conductors().isEmpty() + && !usual_wire) + usual_wire = element->conductors().first(); + check(usual_wire, "a wire on a folio report"); + auto potential = [](Conductor *wire) { + QStringList list; + for (Conductor *c : wire->relatedPotentialConductors(true)) + list << c->uuid().toString(); + list.sort(); + return list; + }; + QETProject followed(path); + Diagram *folio = followed.folios().at(eager.folioIndex(usual_wire->diagram())); + followed.buildFolioToShow(folio); + Conductor *wire = nullptr; + for (Conductor *c : folio->conductors()) + if (c->uuid() == usual_wire->uuid()) wire = c; + check(wire, "the same wire"); + check(potential(wire) == potential(usual_wire), "a potential followed as when built"); + std::printf("potential through reports: %d wires, %d folios not built\n", + int(potential(wire).size()), followed.unloadedFolioCount()); + } + std::printf("PASS: %d folios\n", int(folios.size())); + } catch (const std::exception &error) { + std::printf("FAIL: %s\n", error.what()); + std::fflush(stdout); + return 1; + } + std::fflush(stdout); + return 0; +} diff --git a/tests/qttest/run_lazy_folios_probe.py b/tests/qttest/run_lazy_folios_probe.py new file mode 100644 index 000000000..2c7f881cb --- /dev/null +++ b/tests/qttest/run_lazy_folios_probe.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Link the lazy folios integration probe using an existing Ninja app build. + +Reuses app objects without duplicating the app or introducing a core-library +refactor. No application source or existing executable is replaced. The +generated manifest, probe binary, reports and export fixtures stay in build/. +""" +import argparse +import pathlib +import re +import shutil +import subprocess +import sys + +parser = argparse.ArgumentParser() +parser.add_argument('--build', required=True) +parser.add_argument('--binary', required=True) +parser.add_argument('--source', required=True) +parser.add_argument('--fixture', required=True) +args = parser.parse_args() +build = pathlib.Path(args.build).resolve() +binary = pathlib.Path(args.binary).resolve() +source = pathlib.Path(args.source).resolve() +fixture = pathlib.Path(args.fixture).resolve() +manifest = (build / 'build.ninja').read_text(encoding='utf-8') +match = re.search(r'^build (\S*?/sources/main\.cpp\.(?:obj|o)):', manifest, re.MULTILINE) +if not match: + sys.exit('Cannot find the application main object in the Ninja build.') +main_object = match.group(1) +probe_object = main_object.replace('main.cpp.', 'lazy_folios_probe.cpp.') +probe_binary = binary.with_name('lazy_folios_probe' + binary.suffix) +def escape(path): + return pathlib.Path(path).as_posix().replace('$', '$$').replace(':', '$:').replace(' ', '$ ') +main_source = source.parents[2] / 'sources' / 'main.cpp' +manifest = manifest.replace(main_object, probe_object) +manifest = manifest.replace(escape(main_source), escape(source)) +if binary.suffix: + manifest = manifest.replace(binary.name, probe_binary.name) +else: + old_target = escape(binary.name if binary.parent == build else binary) + new_target = escape(probe_binary.name if probe_binary.parent == build else probe_binary) + manifest = manifest.replace('build ' + old_target + ':', 'build ' + new_target + ':') + old_file = binary.name if binary.parent == build else binary.as_posix() + new_file = probe_binary.name if probe_binary.parent == build else probe_binary.as_posix() + manifest = re.sub(r'^ TARGET_FILE = ' + re.escape(old_file) + r'$', + ' TARGET_FILE = ' + new_file, manifest, flags=re.MULTILINE) +probe_manifest = build / 'lazy-folios-probe.ninja' +probe_manifest.write_text(manifest, encoding='utf-8') +ninja = shutil.which('ninja') +if not ninja: + sys.exit('Ninja is required for the integration probe.') +target = probe_binary.name if probe_binary.parent == build else str(probe_binary) +subprocess.run([ninja, '-C', str(build), '-f', probe_manifest.name, target], check=True) +output = build / 'lazy-folios' +output.mkdir(exist_ok=True) +report = output / 'results.txt' +result = subprocess.run([str(probe_binary), str(fixture), str(report), str(output)], timeout=500) +if report.exists(): + print(report.read_text(encoding='utf-8', errors='replace')) +sys.exit(result.returncode) diff --git a/tests/qttest/tst_resaveunchanged.cpp b/tests/qttest/tst_resaveunchanged.cpp index 46178c692..1babd2c55 100644 --- a/tests/qttest/tst_resaveunchanged.cpp +++ b/tests/qttest/tst_resaveunchanged.cpp @@ -317,6 +317,169 @@ private slots: QCOMPARE(without_save_stamps(read(back)), without_save_stamps(read(straight))); } + // QElectroTech run in a sandbox of its own with @p args, the store + // check on (QET_CHECK_ELEMENT_INFO_STORE); its output + QString runChecked(const QStringList &args, bool eager = false) + { + const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run++)); + QDir().mkpath(home); + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen")); + env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1")); + env.insert(QStringLiteral("QET_CHECK_ELEMENT_INFO_STORE"), QStringLiteral("1")); + if (eager) { + env.remove(QStringLiteral("QET_LAZY_FOLIOS")); //every folio built on opening + } + env.insert(QStringLiteral("HOME"), home); + env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config")); + env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data")); + QProcess proc; + proc.setProcessEnvironment(env); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), args); + if (!proc.waitForFinished(180000)) return {}; + return QString::fromUtf8(proc.readAllStandardOutput() + proc.readAllStandardError()); + } + + // The stores of symbol information and folio title blocks + // (DB-ACCESSORS-PLAN.md stages 4.1, 4.4) agree with every placed symbol + // and folio once a project is open: every example. + void elementInfoStoreAgrees_data() + { + QTest::addColumn("project"); + const QDir examples(QStringLiteral(QET_EXAMPLES_DIR)); + for (const QString &f : examples.entryList({QStringLiteral("*.qet")}, QDir::Files, QDir::Name)) + QTest::newRow(f.toUtf8().constData()) << examples.filePath(f); + } + void elementInfoStoreAgrees() + { + QFETCH(QString, project); + const QString out = runChecked({QStringLiteral("--resave"), project, + m_dir.filePath(QStringLiteral("store%1.qet").arg(m_run))}); + QVERIFY2(out.contains(QStringLiteral("element information store: 0 differ")), qPrintable(out.right(600))); + QVERIFY2(out.contains(QStringLiteral("folio title block store: 0 differ")), qPrintable(out.right(600))); + QVERIFY2(out.contains(QStringLiteral("wire properties store: 0 differ")), qPrintable(out.right(600))); + } + + // The stores filled from the document before the folios are built + // (DB-ACCESSORS-PLAN.md stage 4.6) hold what building them gives, for a + // project saved by this version (every symbol, folio and wire carries + // a uuid): nothing differs, nothing is left unplaced. + void storesFilledBeforeBuilding() + { + const QString saved = resave(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet")); + QVERIFY2(!saved.isEmpty(), "--resave failed"); + const QString out = runChecked({QStringLiteral("--resave"), saved, + m_dir.filePath(QStringLiteral("prefilled.qet"))}, true); + const QRegularExpression line(QStringLiteral( + "filled before building: symbols (\\d+) agree, 0 differ, 0 not placed; " + "folios (\\d+) agree, 0 differ, 0 not placed; wires (\\d+) agree, 0 differ, 0 not placed")); + const QRegularExpressionMatch m = line.match(out); + QVERIFY2(m.hasMatch(), qPrintable(out.right(600))); + QVERIFY(m.captured(1).toInt() > 600 && m.captured(2).toInt() == 50 && m.captured(3).toInt() > 600); + } + + // Title block fields stamped on every folio go through the store, and + // the saved folios carry them (a folio saves what the store holds). + void folioTitleBlockStoreAfterSetTitleBlock() + { + const QString saved = m_dir.filePath(QStringLiteral("stamped.qet")); + const QString out = runChecked({QStringLiteral("--set-titleblock"), + QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), saved, + QStringLiteral("author=Ann"), QStringLiteral("extrafield=Hello")}); + QVERIFY2(out.contains(QStringLiteral("folio title block store: 0 differ")), qPrintable(out.right(600))); + const QByteArray xml = read(saved); + QCOMPARE(xml.count(" wires ? ' with wires' : ''));\n" + "qet.log('ROWS ' + rows());\n" + "qet.log('SAVED ' + qet.save('%1'));\n").arg(saved)).toUtf8()); + js.close(); + QFile js2(dump); + QVERIFY(js2.open(QIODevice::WriteOnly)); + js2.write((rows + QStringLiteral("qet.log('ROWS ' + rows());\n")).toUtf8()); + js2.close(); + + const QString edited = runChecked({QStringLiteral("--run"), edit, + QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet")}); + QVERIFY2(edited.contains(QStringLiteral("COPIED 3")) && edited.contains(QStringLiteral("WIRED 6 with wires")) + && edited.contains(QStringLiteral("UNDONE true")) + && edited.contains(QStringLiteral("SAVED true")), qPrintable(edited.right(600))); + const QString reopened = runChecked({QStringLiteral("--run"), dump, saved}); + auto rowsOf = [](const QString &log) { + const int at = log.indexOf(QStringLiteral("ROWS ")); + return at < 0 ? QString() : log.mid(at + 5, log.indexOf(QLatin1Char('\n'), at) - at - 5); + }; + QVERIFY(!rowsOf(reopened).isEmpty()); + QCOMPARE(rowsOf(edited), rowsOf(reopened)); + } + + // A title-block value that is a single space is kept through two saves // (#973), and a value with accents comes back as it went in. void singleSpaceValueKept()