From 0bcd0ca4b6f8823fd12a53060af69aed65d83f42 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Mon, 19 Oct 2026 16:45:55 +1300 Subject: [PATCH] Keep the symbols and wires a folio cannot load, and save them back Opening a project dropped, with only a debug line, a symbol whose definition could not be found (missing from the project's collection and from the library) and every wire on it; and a wire whose terminal could not be matched, after the symbol's definition was redrawn. The next save wrote the project without them. One save of a forum upload whose collection was incomplete kept 1 of 219 symbols and none of its 299 wires. A folio now keeps what it could not load as it was read (Diagram::m_kept_symbols, m_kept_wires) and writes it back when it is saved whole (Diagram::writeKept()): - a symbol, its terminals given ids after those of the folio's symbols; - a wire, an end found written from its terminal as Conductor::toXml() does, an end not found as the symbol and terminal uuids it names; for a wire saved by legacy ids, the terminal's uuid derived from where it was saved (Element::savedTerminalUuid(), out of matchMovedTerminals()), so it joins again once the definition has it there. A wire whose ends cannot be read, or an end naming no terminal of the file, is kept so that it joins nothing. A symbol kept counts as waiting (projectDataBase::waitingSymbols()), so a partner keeps its link to it. The open warning now lists the symbols not loaded too, and says both are kept. Once the definition is back, the symbol and its wires are built again: 741.qet with one transistor definition removed (8 symbols, 33 wires on them) saves all 67 wires, and with the definition put back opens with all 65 symbols and 67 wires, the same folio as before. tst_resaveunchanged::unloadableItemsKept: fixtures/coil_contact_wired.qet (coil, linked contact, two wires; built with the script API) with the coil's definition removed, or the contact's terminals moved and given new uuids: both symbols, wires and links saved, a second save the same, and the coil's definition put back gives the undamaged project's symbols and wires. Fails on master (1 symbol; 0 wires). FINDINGS F127, F128. Co-Authored-By: Claude Opus 5.5 --- sources/dataBase/projectdatabase.cpp | 17 +- sources/dataBase/projectdatabase.h | 1 + sources/diagram.cpp | 210 ++++++++- sources/diagram.h | 25 ++ sources/qetdiagrameditor.cpp | 37 +- sources/qetgraphicsitem/element.cpp | 63 +-- sources/qetgraphicsitem/element.h | 1 + tests/qttest/fixtures/coil_contact_wired.qet | 421 +++++++++++++++++++ tests/qttest/tst_resaveunchanged.cpp | 95 +++++ 9 files changed, 823 insertions(+), 47 deletions(-) create mode 100644 tests/qttest/fixtures/coil_contact_wired.qet diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index e8c7b92b2..b2b9d3da3 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -4119,9 +4119,24 @@ bool projectDataBase::savedLinks(const Element *element, QList projectDataBase::waitingSymbols() const +{ + QSet kept; + if (m_project) + for (Diagram *diagram : m_project->folios()) + if (diagram->isLoaded()) kept.unite(diagram->keptSymbolUuids()); + return unbuiltSymbols().unite(kept); +} + +/** + @brief projectDataBase::unbuiltSymbols + @return the symbols on folios not built yet and not built alone + (waitingSymbols()) +*/ +QSet projectDataBase::unbuiltSymbols() const { if (!m_project || !m_project->unloadedFolioCount()) { return QSet(); diff --git a/sources/dataBase/projectdatabase.h b/sources/dataBase/projectdatabase.h index da8d4103b..2c1118a58 100644 --- a/sources/dataBase/projectdatabase.h +++ b/sources/dataBase/projectdatabase.h @@ -165,6 +165,7 @@ class projectDataBase : public QObject QList> linkRowsOf(const QUuid &element) const; QSet waitingPartners(const QUuid &element, const QSet &waiting); QSet waitingSymbols() const; + QSet unbuiltSymbols() const; //Fill the three stores from a project's document before its //folios are built, then, once diff --git a/sources/diagram.cpp b/sources/diagram.cpp index 9940ced53..8d08da012 100644 --- a/sources/diagram.cpp +++ b/sources/diagram.cpp @@ -944,6 +944,132 @@ QStringList Diagram::wiresNotReconnected() const return m_wires_not_reconnected; } +/** + @brief Diagram::symbolsNotLoaded + @return one line per symbol of the loaded file that could not be built, + its definition missing from the project and the collections: kept as + read and saved back (F127). Empty for a folio that built every symbol. +*/ +QStringList Diagram::symbolsNotLoaded() const +{ + return m_symbols_not_loaded; +} + +/** + @brief Diagram::keptSymbolUuids + @return the uuids of the symbols this folio could not build and keeps + as read: a partner linked to one of them keeps its link +*/ +QSet Diagram::keptSymbolUuids() const +{ + QSet uuids; + for (const QDomElement &symbol : m_kept_symbols) + uuids.insert(QUuid(symbol.attribute(QStringLiteral("uuid")))); + return uuids; +} + +/** + @brief Diagram::keepWire + Keep @p wire, which could not be joined to its terminals, to save it + back (F127, F128). An end found (@p found1, @p found2) is written from + that terminal at save; an end not found keeps the symbol and terminal + it names, as uuids: for a wire saved by legacy terminal ids, the + symbol among @p symbols (uuid and XML of each symbol read) holding that + id and the uuid derived for that terminal where it was saved + (Element::savedTerminalUuid()), so the wire joins it again once the + symbol's definition has it there. An end naming no terminal of the + file at all pointed nowhere when it was saved: written as the null + uuid, so it names no symbol and is read back as a wire whose ends + cannot be read, kept unchanged and joining nothing (never a legacy id, + which the next save gives to another terminal). +*/ +void Diagram::keepWire(const QDomElement &wire, Terminal *found1, Terminal *found2, + const QList> &symbols) +{ + KeptWire kept; + Terminal *found[2] = {found1, found2}; + for (int i = 0 ; i < 2 ; ++i) { + const QString index = QString::number(i + 1); + if (found[i]) { + kept.end[i] = found[i]; + kept.found[i] = true; + continue; + } + if (wire.hasAttribute(QStringLiteral("element") + index)) { + kept.element[i] = QUuid(wire.attribute(QStringLiteral("element") + index)); + kept.terminal[i] = QUuid(wire.attribute(QStringLiteral("terminal") + index)); + } else { + const QString id = wire.attribute(QStringLiteral("terminal") + index); + for (const auto &symbol : symbols) { + const QList records = QET::findInDomElement( + symbol.second, QStringLiteral("terminals"), QStringLiteral("terminal")); + for (int r = 0 ; r < records.size() && kept.element[i].isNull() ; ++r) + if (records.at(r).attribute(QStringLiteral("id")) == id) { + kept.element[i] = symbol.first; + kept.terminal[i] = Element::savedTerminalUuid(records, r); + } + if (!kept.element[i].isNull()) break; + } + } + } + kept.xml = m_kept_document.importNode(wire, true).toElement(); + if (QUuid(kept.xml.attribute(QStringLiteral("uuid"))).isNull() && m_project) { + QStringList ends; + for (int i = 0 ; i < 2 ; ++i) + ends << (kept.found[i] + ? kept.end[i]->parentElement()->uuid().toString() + QLatin1Char('/') + + kept.end[i]->stableUuid().toString() + : kept.element[i].toString() + QLatin1Char('/') + kept.terminal[i].toString()); + ends.sort(); + kept.xml.setAttribute(QStringLiteral("uuid"), + m_project->derivedItemUuid(QStringLiteral("conductor"), + ends.join(QLatin1Char('\n'))).toString()); + } + m_kept_wires << kept; +} + +/** + @brief Diagram::writeKept + Write what this folio could not load, as it was read, into @p elements + and @p conductors of @p document: the symbols, their terminals given ids + after those of @p table_adr_id so none is read twice, and the wires, an + end found written from its terminal as Conductor::toXml() does. A wire + whose found end was deleted since goes with it. +*/ +void Diagram::writeKept(QDomDocument &document, QDomElement &elements, + QDomElement &conductors, const QHash &table_adr_id) const +{ + int next_id = 0; + for (int id : table_adr_id) next_id = qMax(next_id, id + 1); + for (const QDomElement &symbol : m_kept_symbols) { + QDomElement copy = document.importNode(symbol, true).toElement(); + for (QDomElement record : QET::findInDomElement(copy, QStringLiteral("terminals"), + QStringLiteral("terminal"))) + record.setAttribute(QStringLiteral("id"), next_id++); + elements.appendChild(copy); + } + for (const KeptWire &kept : m_kept_wires) { + if ((kept.found[0] && !kept.end[0]) || (kept.found[1] && !kept.end[1])) continue; + QDomElement copy = document.importNode(kept.xml, true).toElement(); + for (int i = 0 ; i < 2 && !kept.as_read ; ++i) { + const QString index = QString::number(i + 1); + const QString element = QStringLiteral("element") + index; + const QString terminal = QStringLiteral("terminal") + index; + if (!kept.found[i]) { + copy.setAttribute(element, kept.element[i].toString()); + copy.setAttribute(terminal, kept.terminal[i].toString()); + } else if (kept.end[i]->uuid().isNull()) { + copy.removeAttribute(element); + copy.setAttribute(terminal, table_adr_id.value(kept.end[i])); + } else { + copy.setAttribute(element, kept.end[i]->parentElement()->uuid().toString()); + copy.setAttribute(terminal, kept.end[i]->uuid().toString()); + } + } + conductors.appendChild(copy); + } +} + /** @brief Diagram::uuidUsedByOtherDiagram A hand-edited or merged project file can contain two folios with the same @@ -1268,7 +1394,10 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) { } document.appendChild(dom_root); - if (items().isEmpty()) + //What the folio could not load is written back with a whole folio + const bool write_kept = whole_content && !is_copy_command + && (!m_kept_symbols.isEmpty() || !m_kept_wires.isEmpty()); + if (items().isEmpty() && !write_kept) return(document); QVector list_elements; @@ -1375,8 +1504,8 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) { // table de correspondance entre les adresses des bornes et leurs ids QHash table_adr_id; - if (!list_elements.isEmpty()) { - auto dom_elements = document.createElement(QStringLiteral("elements")); + auto dom_elements = document.createElement(QStringLiteral("elements")); + if (!list_elements.isEmpty() || write_kept) { for (auto elmt : list_elements) { QDomElement dom_element = elmt->toXml(document, table_adr_id); ItemGroups::write(dom_element, elmt); @@ -1388,6 +1517,7 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) { dom_root.appendChild(dom_elements); } + auto dom_conductors = document.createElement(QStringLiteral("conductors")); if (!list_conductors.isEmpty()) { //Symbols copied in old versions can share one uuid. A wire //saved by that uuid would reopen on the first of them (#1408). @@ -1397,7 +1527,6 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) { shared_uuids.insert(elmt->uuid()); seen_uuids.insert(elmt->uuid()); } - auto dom_conductors = document.createElement(QStringLiteral("conductors")); for (auto cond : list_conductors) { dom_conductors.appendChild(cond->toXml(document, table_adr_id, @@ -1405,6 +1534,11 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) { } dom_root.appendChild(dom_conductors); } + if (write_kept) { + writeKept(document, dom_elements, dom_conductors, table_adr_id); + if (dom_conductors.hasChildNodes() && dom_conductors.parentNode().isNull()) + dom_root.insertAfter(dom_conductors, dom_elements); + } // A rect query, unlike items(), returns true stacking order (z, then // insertion order) even with NoIndex. @@ -1603,6 +1737,23 @@ void Diagram::folioSequentialsToXml(QHashderivedItemUuid( + QStringLiteral("element"), + QStringList{element_xml.attribute(QStringLiteral("type")), + element_xml.attribute(QStringLiteral("x")), + element_xml.attribute(QStringLiteral("y")), + element_xml.attribute(QStringLiteral("orientation"))} + .join(QLatin1Char('\n'))); +} + Element *Diagram::buildElement(QDomElement element_xml, QHash &table_adr_id, bool consider_informations) { @@ -1650,13 +1801,7 @@ Element *Diagram::buildElement(QDomElement element_xml, QHash & //uuids anyway. if (consider_informations && m_project && QUuid(element_xml.attribute(QStringLiteral("uuid"))).isNull()) { - nvel_elmt->setUuid(m_project->derivedItemUuid( - QStringLiteral("element"), - QStringList{type_id, - element_xml.attribute(QStringLiteral("x")), - element_xml.attribute(QStringLiteral("y")), - element_xml.attribute(QStringLiteral("orientation"))} - .join(QLatin1Char('\n')))); + nvel_elmt->setUuid(derivedSymbolUuid(element_xml)); } ItemGroups::setGroup(nvel_elmt, ItemGroups::read(element_xml)); return nvel_elmt; @@ -2092,6 +2237,14 @@ bool Diagram::fromXml(QDomElement &document, QHash table_adr_id; if (m_loading_deferred) { table_adr_id = m_partial_table; //the terminals of symbols built already + } + //Each symbol read, by uuid: the ends of a wire kept (keepWire()) + QList> read_symbols; + if (consider_informations) { + m_kept_document = QDomDocument(); + m_kept_symbols.clear(); + m_kept_wires.clear(); + m_symbols_not_loaded.clear(); } for (auto element_xml : QET::findInDomElement(root, QStringLiteral("elements"), QStringLiteral("element"))) @@ -2102,11 +2255,30 @@ bool Diagram::fromXml(QDomElement &document, Element *built = m_partial_elements.value(QUuid(element_xml.attribute(QStringLiteral("uuid")))); if (built) { added_elements << built; + read_symbols.append({built->uuid(), element_xml}); continue; } } - if (Element *nvel_elmt = buildElement(element_xml, table_adr_id, consider_informations)) + if (Element *nvel_elmt = buildElement(element_xml, table_adr_id, consider_informations)) { added_elements << nvel_elmt; + if (consider_informations) read_symbols.append({nvel_elmt->uuid(), element_xml}); + } else if (consider_informations) { + //Its definition is missing: kept as read, saved back, so a + //save loses nothing (F127) + QDomElement kept = m_kept_document.importNode(element_xml, true).toElement(); + if (QUuid(kept.attribute(QStringLiteral("uuid"))).isNull() && m_project) + kept.setAttribute(QStringLiteral("uuid"), derivedSymbolUuid(element_xml).toString()); + m_kept_symbols << kept; + read_symbols.append({QUuid(kept.attribute(QStringLiteral("uuid"))), element_xml}); + QString label; + for (const QDomElement &info : QET::findInDomElement( + element_xml, QStringLiteral("elementInformations"), QStringLiteral("elementInformation"))) + if (info.attribute(QStringLiteral("name")) == QLatin1String("label")) label = info.text(); + const QString type = element_xml.attribute(QStringLiteral("type")); + m_symbols_not_loaded << (label.isEmpty() + ? type.section(QLatin1Char('/'), -1) + : QStringLiteral("%1 (%2)").arg(label, type.section(QLatin1Char('/'), -1))); + } } //Texts, images and shapes written before they carried a uuid (or @@ -2262,7 +2434,17 @@ bool Diagram::fromXml(QDomElement &document, QStringLiteral("conductors"), QStringLiteral("conductor"))) { - if (!Conductor::valideXml(f)) continue; + if (!Conductor::valideXml(f)) { + //Its ends cannot be read: kept and written back unchanged + //(F128), it can join nothing + if (consider_informations) { + KeptWire kept; + kept.xml = m_kept_document.importNode(f, true).toElement(); + kept.as_read = true; + m_kept_wires << kept; + } + continue; + } //Check if terminal that conductor must be linked is know @@ -2297,6 +2479,8 @@ bool Diagram::fromXml(QDomElement &document, if (!num.isEmpty() && num != QLatin1String("_")) wire += QStringLiteral(" (%1)").arg(num); m_wires_not_reconnected << wire; + //Kept as read, saved back (F128) + keepWire(f, p1, p2, read_symbols); } if (p1 && p2 && p1 != p2) diff --git a/sources/diagram.h b/sources/diagram.h index cddb4d77e..614e5a184 100644 --- a/sources/diagram.h +++ b/sources/diagram.h @@ -39,6 +39,7 @@ class DiagramContent; class DiagramPosition; class DiagramTextItem; class Element; +class Terminal; class ElementsLocation; class DiagramImageItem; class DiagramEventInterface; @@ -165,6 +166,28 @@ class Diagram : public QGraphicsScene //Wires of the loaded file whose ends could not be found QStringList m_wires_not_reconnected; + //Symbols of the loaded file that could not be built + QStringList m_symbols_not_loaded; + + //What the folio could not load, written back as it was read + //(F127, F128): symbols whose definition is missing, and wires + //to a terminal not found + struct KeptWire { + QDomElement xml; + QPointer end[2]; ///< an end found, null if not + bool found[2] = {false, false}; + QUuid element[2]; ///< an end not found: its symbol + QUuid terminal[2]; ///< and its terminal + bool as_read = false; ///< no end can be read: written unchanged + }; + QDomDocument m_kept_document; + QList m_kept_symbols; + QList m_kept_wires; + QUuid derivedSymbolUuid(const QDomElement &element_xml) const; + void keepWire(const QDomElement &wire, Terminal *found1, Terminal *found2, + const QList> &symbols); + void writeKept(QDomDocument &document, QDomElement &elements, + QDomElement &conductors, const QHash &table_adr_id) const; // METHODS protected: @@ -189,6 +212,8 @@ class Diagram : public QGraphicsScene void restoreText(Element* elmt); QUuid uuid(); QStringList wiresNotReconnected() const; + QStringList symbolsNotLoaded() const; + QSet keptSymbolUuids() const; void setEventInterface (DiagramEventInterface *event_interface); void clearEventInterface(); diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index ed35d1085..c3a3016c6 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -2262,15 +2262,38 @@ bool QETDiagramEditor::openAndAddProject( ); } - //Report wires left out of the load because a terminal they join - //was not found: they would otherwise vanish on the next save - //without the user ever being told. - QStringList lost_wires; + //Report the elements and wires left out of the load: an element + //whose definition is missing, a wire whose terminal was not found. + //They are kept in the file as they were (F127, F128), but are not + //shown. + QStringList lost_elements, lost_wires; + for (Diagram *diagram : project->folios()) { + if (!diagram->isLoaded()) continue; + for (const QString &element : diagram->symbolsNotLoaded()) + lost_elements << tr("Sheet %1: %2").arg(diagram->folioIndex() + 1).arg(element); + } for (Diagram *diagram : project->foliosWithWiresNotReconnected()) { for (const QString &wire : diagram->wiresNotReconnected()) { lost_wires << tr("Sheet %1: %2").arg(diagram->folioIndex() + 1).arg(wire); } } + if (interactive && !lost_elements.isEmpty()) + { + QMessageBox box(QMessageBox::Warning, + tr("Elements not loaded", "message box title"), + tr("%n elements could not be loaded: their definition is " + "missing from the project and from the collections.\n" + "\n" + "They are not shown, but they are kept in the file as they " + "are, with their conductors, and come back once their " + "definition is available again.", + "message box content", + lost_elements.size()), + QMessageBox::Ok, + this); + box.setDetailedText(lost_elements.join(QLatin1Char('\n'))); + box.exec(); + } if (interactive && !lost_wires.isEmpty()) { QMessageBox box(QMessageBox::Warning, @@ -2280,9 +2303,9 @@ bool QETDiagramEditor::openAndAddProject( "was probably replaced by another one with different " "terminals.\n" "\n" - "If you save the project, these conductors will disappear " - "from the file. Close it without saving to keep the file as " - "it is.", + "They are not shown, but they are kept in the file as they " + "are, and join their terminals again once the definition " + "has them where they were.", "message box content", lost_wires.size()), QMessageBox::Ok, diff --git a/sources/qetgraphicsitem/element.cpp b/sources/qetgraphicsitem/element.cpp index e9eacba5f..2d487a1aa 100644 --- a/sources/qetgraphicsitem/element.cpp +++ b/sources/qetgraphicsitem/element.cpp @@ -975,6 +975,41 @@ bool Element::valideXml(QDomElement &e) return(true); } +/** + @brief Element::savedTerminalUuid + @return the uuid TerminalUuids derives for the terminal saved as + @p records [@p index] (a symbol's saved terminals, in file order) at the + place it was saved: its docking point less the stub Terminal::init() + adds, and its rank among the records at that place, as the project + counted it when it gave the terminals their uuids +*/ +QUuid Element::savedTerminalUuid(const QList &records, int index) +{ + const auto dock_of = [](const QDomElement &r) { + return QPointF(r.attribute(QStringLiteral("x")).toDouble(), + r.attribute(QStringLiteral("y")).toDouble()); + }; + const QDomElement &record = records.at(index); + const int orientation = record.attribute(QStringLiteral("orientation")).toInt(); + const QPointF dock = dock_of(record); + QPointF old_pos = dock; + switch (orientation) { + case Qet::North: old_pos -= QPointF(0, Terminal::terminalSize); break; + case Qet::East : old_pos -= QPointF(-Terminal::terminalSize, 0); break; + case Qet::West : old_pos -= QPointF(Terminal::terminalSize, 0); break; + case Qet::South: old_pos -= QPointF(0, -Terminal::terminalSize); break; + } + int rank = 0; + for (int j = 0; j < index; ++j) { + const QPointF other = dock_of(records.at(j)); + if (records.at(j).attribute(QStringLiteral("orientation")).toInt() == orientation + && qAbs(other.x() - dock.x()) < 1e-6 + && qAbs(other.y() - dock.y()) < 1e-6) + ++rank; + } + return TerminalUuids::derived(old_pos.x(), old_pos.y(), orientation, rank); +} + /** @brief Element::matchMovedTerminals Pair the saved terminals that no live terminal matched exactly with the @@ -1033,32 +1068,8 @@ void Element::matchMovedTerminals(const QList &records, //Pair a record with a live terminal: register its legacy id, and the //uuid derived for the terminal at its old place const auto pair_up = [&](int index, Terminal *t) { - const QDomElement &record = records.at(index); - const QPointF dock = dock_of(record); - - //The old place of the terminal: its docking point less the - //stub that Terminal::init() added - QPointF old_pos = dock; - switch (orientation_of(record)) { - case Qet::North: old_pos -= QPointF(0, Terminal::terminalSize); break; - case Qet::East : old_pos -= QPointF(-Terminal::terminalSize, 0); break; - case Qet::West : old_pos -= QPointF(Terminal::terminalSize, 0); break; - case Qet::South: old_pos -= QPointF(0, -Terminal::terminalSize); break; - } - //The rank among the records at the same place, as the project - //counted it when it gave the terminals their uuids - int rank = 0; - for (int j = 0; j < index; ++j) { - const QPointF other = dock_of(records.at(j)); - if (orientation_of(records.at(j)) == orientation_of(record) - && qAbs(other.x() - dock.x()) < 1e-6 - && qAbs(other.y() - dock.y()) < 1e-6) - ++rank; - } - - id_adr.insert(record.attribute(QStringLiteral("id")).toInt(), t); - m_old_place_terminals.insert(TerminalUuids::derived(old_pos.x(), old_pos.y(), - orientation_of(record), rank), t); + id_adr.insert(records.at(index).attribute(QStringLiteral("id")).toInt(), t); + m_old_place_terminals.insert(savedTerminalUuid(records, index), t); m_moved_terminals << t; }; const auto same_place = [](const QPointF &a, const QPointF &b) { diff --git a/sources/qetgraphicsitem/element.h b/sources/qetgraphicsitem/element.h index 22384b9d9..b5f6764f5 100644 --- a/sources/qetgraphicsitem/element.h +++ b/sources/qetgraphicsitem/element.h @@ -183,6 +183,7 @@ class Element : public QetGraphicsItem QPoint hotspot() const; void editProperty() override; static bool valideXml(QDomElement &); + static QUuid savedTerminalUuid(const QList &records, int index); virtual bool fromXml( QDomElement &, QHash + + 10/2/26 + 02-10-2026 + 2026-10-02 + free_text_width + 18:39 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + K1 + + + + A2 + + + A1 + + + K1 + label + + + + + + + + + + + + + + + + K1 + label + + + + + + + + + + + + + + + + + + + + + + + Imported elements + + + + كهربائي + Elèctric + Elektrotechnika + Elektrisk + Elektrik + Ηλεκτρικά + Electric + Eléctrica + Sähkö + Electrique + Električno + Elektromos + Elettrica + 電気 + 전기 + Цахилгаан + Elektrisk + Elektrotechniek + Elektrisch + Elektrotechnika + Elétrico + Elétrica + Electric + Электротехника + Elektrické + Elektrika + Електрика + El + Elektrik + Електрика + 10_电气 + 电气工程 + + + + متعدد الأقطاب + Multifilar + Vícežilový + Flere ledere + Allpolig + Πολυγραμμικό + All-pole + Multifilar + Yleisnapa + Multifilaire + Univerzalni + Többvonalas szimbólumok + Multifilare + 複線 + 다-극 + Бүх туйл + Alle poler + Veel polig + All-pole + Schematy wieloliniowe + Multipolar + Multifilar + Bipolar + Многополюсные + Všepólové + Vsi poli + Свe-полни + Allpolig + Çok kutuplu + Багатополюсні + 10_全极 + + + + المرحّلات والموصّلات وجهات الاتصال + Relés, contactors i contactes + Relé, stykače a kontakty + Relæer, kontaktorer og kontakter + Relais, Schütze und Kontakte + Ηλεκτρονόμοι και επαφές + Relays, contactors and contacts + Relevadores, contactores y contactos + Releet, kontaktorit ja koskettimet + Relais, contacteurs et contacts + Releji, kontaktori i kontakti + Relék, kontaktorok és érintkezők + Relè, contattori e contatti + 継電器、接触器、接点 + 릴레이, 커넥터및 접점 + Реле, контактор болон контакт + Reléer, kontaktorere og kontakter + Relais, contactoren en contacten + Relais, contacteurs en contacten + Przekaźniki i styczniki + Relés, contatores e contactos + Relés, Contatores e contatos + Relee, contactoare și contacte + Реле, пускатели и контакты + Relé, stykače a kontakty + Releji, kontaktorji in kontakti + Релеји, контактори и контакти + Reläer, kontaktorer och kontakter + Röleler, kontaktörler ve kontaklar + Реле, контактори та контакти + 继电器、接触器与触点 + + + + لفه + Bobines + cívka + spole + Spule + σπείρα + coil + bobina + Käämi + bobine + zavojnica + tekercs + bobina + コイル + 코일 + ороомог + Spole + spoel + Spoel + cewka + bobina + bobina + bobina + катушка + Cievka + tuljava + калем + spole + bobin + котушка + 线圈 + + + + + + ملف + Bobina + Cívka relé + Relæ + Spule + Πηνίο + Coil + Bobina + Käämi + Bobine + Zavojnica + Tekercs + Bobina + コイル + 코일 + Ороомог + Spole + Spoel + Relais spoel (algemeen) + Cewka + Bobina + Bobina + Bobina + Обмотка + Cievka + Tuljava + Каlem + Spole + Bobin + Котушка + 三线圈 + + + coil + + Author: The QElectroTech team +License: see http://qelectrotech.org/wiki/doc/elements_license + + + + + A2 + + + A1 + + + + + label + + + + + + + + + + تماسات مع إحالات + Contactes amb referències creuades + Kontakty s křížovými odkazy + Kontaker med krydsreference + Kontakte mit Querverweis + Επαφές με παραπομπές + Contacts with cross referencing + Contactos con referencia cruzada + Kosketimet lehtiviittauksella + Contacts avec référence croisée + Kontakti s unakrsnim referenciranjem + Érintkezők kereszthivatkozásokkal + Contatti con riferimenti incrociati + 相互参照付き接点 + 교차 참조 접점 + Харилцан холбоос бүхий контакт + Kontakter med kryssreferanse + Contacten met kruisreferentie + Contacten met kruisverwijzing + Zestyki (automatyczne oznaczenia) + Contactos com renvoi de folio + Contatos com referencia cruzada + Contacte cu renvoi de folio + Контакты с перекрестными ссылками + Kontakty s krížovým odkazom + Kontakti s sklici + Контакти са унакрсним референцирањем + Kontakter med bladhänvisning + Çapraz referanslı kontaklar + Контакти з перехресними посиланнями + 触点交叉引用 + + + + مساعد + Auxiliars + Pomocné kontakty + Hjælpe kontakter + Hilfsglieder + Βοηθητικές επαφές + Auxiliary + Contactos auxiliares + Apupiiri + Auxilliaires + Pomoćni + Segéd + Contatti ausiliari + 補助 + 보조 접점 + Туслах + Hjelpe + Hulpcontacten + Hulpcontacten + Zestyki pomocnicze + Auxiliar + Auxiliares + Auxiliare + Внешние + Pomocné + Pomožni + Помоћни + Hjälp + Yardımcı + Допоміжні + 辅助触点 + + + + + + مُلامس بسيط (NO) + Contacte senzill + Spínací kontakt + Enkel kontakt + Schliesser + Απλή επαφή, ανοικτή + Simple contact + Contactor simple + Yksinkertainen kosketin + Contact simple + Jednostavni kontakt + Egyszerű érintkező + Contatto semplice (NO) + 単純接点 + 단순 접점 + Энгийн контакт + Enkel kontakt + contact enkel + Eenvoudig contact + Zestyk zwierny + Contacto simples (NO) + Contato simples + Contact simplu + Проктой контакт (НР) + Jednoduchý kontakt + Enostavni kontakt + Прости контакт + Enkelkontakt + Basit kontak + Простий контакт + 简单连接器 + + + simple + NO + 1 + + Author: The QElectroTech team +License: see http://qelectrotech.org/wiki/doc/elements_license + + + + + label + + + + + + + + + + + + + + + diff --git a/tests/qttest/tst_resaveunchanged.cpp b/tests/qttest/tst_resaveunchanged.cpp index fe6a90a00..fe428c90b 100644 --- a/tests/qttest/tst_resaveunchanged.cpp +++ b/tests/qttest/tst_resaveunchanged.cpp @@ -802,6 +802,101 @@ private slots: QStringLiteral("]*name=\"accents\"[^>]*>Armoire façade été"))), "the accented value changed"); } + + // What a folio cannot load is saved back as it was read (F127, F128). + // fixtures/coil_contact_wired.qet: a coil K1 and its contact, linked, + // two wires between them. Each row damages it as an uploaded project + // can be: the coil's definition missing from the file, or the + // contact's definition redrawn (terminals moved, other uuids). A save + // keeps both symbols, both wires and both links, a second save changes + // nothing, and once the coil's definition is back the save is the + // undamaged project's again. + void unloadableItemsKept_data() + { + QTest::addColumn("damage"); + QTest::newRow("definition missing") << QStringLiteral("coil"); + QTest::newRow("terminals redrawn") << QStringLiteral("contact"); + } + void unloadableItemsKept() + { + QFETCH(QString, damage); + const QString fixture = QFINDTESTDATA("fixtures/coil_contact_wired.qet"); + const QString whole = resave(fixture); + QVERIFY2(!whole.isEmpty(), "--resave of the fixture failed"); + const QString xml = QString::fromUtf8(read(whole)); + static const QRegularExpression coil_definition(QStringLiteral( + ".*?\\s*"), + QRegularExpression::DotMatchesEverythingOption); + const QString coil = coil_definition.match(xml).captured(0); + QVERIFY(!coil.isEmpty()); + QString damaged = xml; + if (damage == QLatin1String("coil")) { + damaged.remove(coil); + } else { + //The contact's terminals 40 further right, with new uuids + static const QRegularExpression contact_definition(QStringLiteral( + ".*?"), + QRegularExpression::DotMatchesEverythingOption); + QString contact = contact_definition.match(xml).captured(0); + QVERIFY(!contact.isEmpty()); + const QString before = contact; + static const QRegularExpression terminal(QStringLiteral( + "(]*uuid=\"\\{)[0-9a-f]{8}([^>]*\\bx=\")(-?[0-9.]+)")); + int n = 0; + for (auto it = terminal.globalMatch(before); it.hasNext() ; ++n) { + const auto m = it.next(); + contact.replace(m.captured(0), m.captured(1) + QStringLiteral("%1").arg(n, 8, 10, QLatin1Char('0')) + + m.captured(2) + QString::number(m.captured(3).toDouble() + 40)); + } + QCOMPARE(n, 2); + damaged.replace(before, contact); + } + const QString damaged_path = m_dir.filePath(QStringLiteral("damaged%1.qet").arg(m_run)); + QFile out(damaged_path); + QVERIFY(out.open(QIODevice::WriteOnly)); + out.write(damaged.toUtf8()); + out.close(); + + auto count = [](const QString &text, const char *pattern) { + return int(text.count(QRegularExpression(QString::fromLatin1(pattern)))); + }; + const QString first = resave(damaged_path); + QVERIFY2(!first.isEmpty(), "--resave of the damaged project failed"); + const QString saved = QString::fromUtf8(read(first)); + QCOMPARE(count(saved, "]*\\btype="), 2); + QCOMPARE(count(saved, "\\s*.*?\\s*)"), + QRegularExpression::DotMatchesEverythingOption); + QString mended = saved; + QVERIFY(collection_category.match(mended).hasMatch()); + mended.replace(collection_category, QStringLiteral("\\1") + coil); + const QString mended_path = m_dir.filePath(QStringLiteral("mended%1.qet").arg(m_run)); + QFile mend(mended_path); + QVERIFY(mend.open(QIODevice::WriteOnly)); + mend.write(mended.toUtf8()); + mend.close(); + const QString back = resave(mended_path); + QVERIFY2(!back.isEmpty(), "--resave of the mended project failed"); + auto items = [](const QString &text, const char *tag) { + QStringList found; + static const QRegularExpression noise(QStringLiteral(" (element[12]_(label|linked|name)|terminalname[12])=\"[^\"]*\"")); + auto it = QRegularExpression(QStringLiteral("<%1 [^>]*>").arg(QLatin1String(tag))).globalMatch(text); + while (it.hasNext()) found << it.next().captured(0).remove(noise); + found.sort(); + return found; + }; + const QString again = QString::fromUtf8(read(back)); + QCOMPARE(items(again, "element"), items(xml, "element")); + QCOMPARE(items(again, "conductor"), items(xml, "conductor")); + } }; QTEST_APPLESS_MAIN(tst_resaveunchanged)