diff --git a/build-aux/windows/QET64.nsi b/build-aux/windows/QET64.nsi index fc79b780a..529efce82 100644 --- a/build-aux/windows/QET64.nsi +++ b/build-aux/windows/QET64.nsi @@ -338,6 +338,9 @@ Section "" WriteRegStr HKEY_CLASSES_ROOT "qet_diagram_file\DefaultIcon" "" "$final_project_ico" WriteRegStr HKEY_CLASSES_ROOT "qet_diagram_file\shell\open\command" "" '"$final_qet_exe" "%1"' + ; File associations - .qetz, the zipped project: the same file type + WriteRegStr HKEY_CLASSES_ROOT ".qetz" "" "qet_diagram_file" + ; File associations - .elmt WriteRegStr HKEY_CLASSES_ROOT ".elmt" "" "qet_element_file" WriteRegStr HKEY_CLASSES_ROOT "qet_element_file" "" "Element QET" @@ -486,6 +489,7 @@ Section "Uninstall" ; Remove file associations registry keys DeleteRegKey HKEY_CLASSES_ROOT "Applications\qelectrotech.exe" DeleteRegKey HKEY_CLASSES_ROOT ".qet" + DeleteRegKey HKEY_CLASSES_ROOT ".qetz" DeleteRegKey HKEY_CLASSES_ROOT "qet_diagram_file" DeleteRegKey HKEY_CLASSES_ROOT ".elmt" DeleteRegKey HKEY_CLASSES_ROOT "qet_element_file" diff --git a/lang/qet_fr.ts b/lang/qet_fr.ts index 4f4b125bf..440ebec99 100644 --- a/lang/qet_fr.ts +++ b/lang/qet_fr.ts @@ -10898,6 +10898,10 @@ Donnez-leur une autre numérotation d'abord. Management Management + + Project QElectroTech (*.qet *.qetz) + Projets QElectroTech (*.qet *.qetz) + ProjectDBModel @@ -11409,6 +11413,33 @@ Voulez-vous enregistrer les modifications ? window title for a project-less ProjectView Projet + + Zipped project QElectroTech (*.qetz) + filetypes allowed when saving a project file + Projet QElectroTech compressé (*.qetz) + + + Save a copy for an older QElectroTech + dialog title + Enregistrer une copie pour une version antérieure de QElectroTech + + + This project was opened from %1. It is saved as %2, beside it, in the zipped project format; %1 stays as it is. + +To write a file for a version of QElectroTech that cannot open %2, use File > Save a copy for an older QElectroTech. + Ce projet a été ouvert depuis %1. Il est enregistré sous %2, à côté, au format de projet compressé ; %1 reste tel quel. + +Pour écrire un fichier lisible par une version de QElectroTech qui ne peut pas ouvrir %2, utilisez Fichier > Enregistrer une copie pour une version antérieure de QElectroTech. + + + Save as a zipped project + message box title + Enregistrement au format de projet compressé + + + %1 already exists. Replace it? + %1 existe déjà. Le remplacer ? + PropertiesEditorDockWidget @@ -13692,6 +13723,19 @@ Déponter et/ou supprimer les niveaux des bornes concerné afin de pouvoir les s message box title Erreur + + Save a copy for an older QElectroTech + Enregistrer une copie pour une version antérieure de QElectroTech + + + Writes a copy of the current project as a .qet file, which versions of QElectroTech that cannot open a .qetz open + status bar tip + Enregistre une copie du projet courant au format .qet, que savent ouvrir les versions de QElectroTech qui ne peuvent pas ouvrir un .qetz + + + QElectroTech project(*.qet *.qetz);;XML Files (*.xml);;All files (*) + Projets QElectroTech (*.qet *.qetz);;Fichiers XML (*.xml);;Tous les fichiers (*) + QETElementEditor @@ -17242,6 +17286,10 @@ Pour l'activer : Configurer QElectroTech > Général > Projets, ou d Ungroup a set of terminals Dégrouper un ensemble de bornes + + Change the information of the element + Modifier les informations de l'élément + QTextOrientationWidget diff --git a/misc/Info.plist b/misc/Info.plist index 3d138a781..c41a2f03f 100644 --- a/misc/Info.plist +++ b/misc/Info.plist @@ -32,6 +32,7 @@ CFBundleTypeExtensions qet + qetz CFBundleTypeRole Editor diff --git a/misc/org.qelectrotech.qelectrotech.desktop b/misc/org.qelectrotech.qelectrotech.desktop index d76e23964..86876992c 100644 --- a/misc/org.qelectrotech.qelectrotech.desktop +++ b/misc/org.qelectrotech.qelectrotech.desktop @@ -6,7 +6,7 @@ Exec=qelectrotech %F Icon=qelectrotech Terminal=false Type=Application -MimeType=application/x-qet-project;application/x-qet-element;application/x-qet-titleblock; +MimeType=application/x-qet-project;application/x-qelectrotech-project+zip;application/x-qet-element;application/x-qet-titleblock; Categories=Graphics; Keywords=Graphics;Science;Electricity;Engineering; Comment=Edit electrical diagrams. diff --git a/misc/qelectrotech.xml b/misc/qelectrotech.xml index 9c59b116b..cefe25eb5 100644 --- a/misc/qelectrotech.xml +++ b/misc/qelectrotech.xml @@ -34,6 +34,14 @@ QElectroTech 项目文件 + + + QElectroTech zipped project file + + + + + QElectroTech element file diff --git a/misc/qet-mcp/README.md b/misc/qet-mcp/README.md index 98586dc52..1de4752d1 100644 --- a/misc/qet-mcp/README.md +++ b/misc/qet-mcp/README.md @@ -25,6 +25,13 @@ here read the model. ## Tools +Every tool takes a `.qet` or a `.qetz` (the same project, zipped). The +tools that read a project's XML read the `.qet` QElectroTech saves from a +`.qetz`, made once per version of the file with the configured +QElectroTech (`QET_BINARY`); the first read of a big `.qetz` takes as long +as QElectroTech needs to save it. The tools that launch QElectroTech hand +it the `.qetz` itself, and write one when the output path ends in `.qetz`. + | Tool | What it answers | |---|---| | `qet_project_info` | title, format version, folios with their uuids, element and conductor counts | diff --git a/misc/qet-mcp/qet_mcp.py b/misc/qet-mcp/qet_mcp.py index f1157ea3c..58177e01a 100755 --- a/misc/qet-mcp/qet_mcp.py +++ b/misc/qet-mcp/qet_mcp.py @@ -97,11 +97,56 @@ def _root(path: str) -> ET.Element: if not p.is_file(): raise ValueError(f"no such file: {p}") try: - return ET.parse(p).getroot() + return ET.parse(_as_qet(p)).getroot() except ET.ParseError as exc: raise ValueError(f"{p.name} is not parseable XML: {exc}") from exc +# -------------------------------------------------------------------------- +# zipped projects (.qetz) +# -------------------------------------------------------------------------- + +# A .qetz is QElectroTech's zipped project: a database and one file per +# folio. The tools that read a project's XML read the .qet QElectroTech +# saves from it, so they read what QElectroTech reads, whatever the zipped +# format holds; one copy per version of the file (path, size, time). +_QETZ_COPIES: dict = {} + + +def _is_qetz(path) -> bool: + return str(path).lower().endswith(".qetz") + + +def _as_qet(path) -> Path: + """The file the XML tools read for `path`: itself, or for a .qetz the + .qet QElectroTech saves from it (made with the configured binary).""" + p = Path(path).expanduser() + if not _is_qetz(p): + return p + if not p.is_file(): + raise ValueError(f"no such file: {p}") + st = p.stat() + key = (str(p.resolve()), st.st_size, st.st_mtime_ns) + cached = _QETZ_COPIES.get(key) + if cached is not None and cached.is_file(): + return cached + binary = resolve_binary() + if not binary: + raise ValueError(f"{p.name} is a zipped project (.qetz): reading it needs " + "QElectroTech; set QET_BINARY") + folder = Path(tempfile.mkdtemp(prefix="qet-mcp-qetz-")) + out = folder / (p.stem + ".qet") + # Saving as .qet builds every folio: allow a big project its time + result = _run_qet(str(binary), ["--resave", str(p), str(out)], timeout=900) + if not out.is_file() or out.stat().st_size == 0: + shutil.rmtree(folder, ignore_errors=True) + detail = (result.get("stderr") or result.get("stdout") or "").strip()[-400:] + raise ValueError(f"QElectroTech could not read {p.name}" + + (f": {detail}" if detail else "")) + _QETZ_COPIES[key] = out + return out + + def _element_info(el: ET.Element) -> dict: """The bag, as a plain dict.""" out = {} @@ -796,7 +841,7 @@ def _num(v) -> float: def tool_scan(directory: str, tag: str = "conductor", attribute: str = "cable", recursive: bool = True) -> dict: - """Sweep every .qet in a directory, counting how many carry a + """Sweep every .qet and .qetz in a directory, counting how many carry a non-empty `attribute`. This is the corpus question: "3190 conductors, 0 cable values across @@ -805,13 +850,14 @@ def tool_scan(directory: str, tag: str = "conductor", d = Path(directory).expanduser() if not d.is_dir(): raise ValueError(f"not a directory: {d}") - files = sorted(d.rglob("*.qet") if recursive else d.glob("*.qet")) + files = sorted([f for pattern in ("*.qet", "*.qetz") + for f in (d.rglob(pattern) if recursive else d.glob(pattern))]) total = non_empty = 0 per_file, values, unreadable = [], {}, [] for f in files: try: - root = ET.parse(f).getroot() - except ET.ParseError as exc: + root = ET.parse(_as_qet(f)).getroot() + except (ET.ParseError, ValueError) as exc: unreadable.append({"file": f.name, "error": str(exc)}) continue n = ne = 0 @@ -3919,8 +3965,10 @@ def scripts_dir() -> Path: SERVER_INSTRUCTIONS = ( "This server works with QElectroTech, a free editor for electrical " - "diagrams. A project (.qet) holds folios (sheets) of symbols " - "(elements) joined by wires (conductors). Folio indexes count from 0.\n" + "diagrams. A project (.qet, or .qetz: the same project zipped) holds " + "folios (sheets) of symbols (elements) joined by wires (conductors). " + "Every tool takes either; a .qetz is read through QElectroTech, so the " + "first read of a big one takes a while. Folio indexes count from 0.\n" "Start with qet_about: where QElectroTech keeps things, what is " "switched on, the stored scripts, and house_style -- this " "installation's own drawing conventions if the user set any. Follow " diff --git a/misc/qet-mcp/test_qet_mcp.py b/misc/qet-mcp/test_qet_mcp.py index ea85bf798..30492aa4b 100644 --- a/misc/qet-mcp/test_qet_mcp.py +++ b/misc/qet-mcp/test_qet_mcp.py @@ -6476,6 +6476,95 @@ class CorpusIntegration(unittest.TestCase): sb.close() +class ZippedProjectWithoutBinary(unittest.TestCase): + def test_reading_a_qetz_without_qelectrotech_says_what_to_set(self): + """A .qetz is read through QElectroTech (m._as_qet()): with none + configured, the answer names QET_BINARY rather than failing to + parse a zip as XML.""" + with tempfile.TemporaryDirectory() as tmp: + z = Path(tmp) / "p.qetz" + z.write_bytes(b"PK\x03\x04 not read") + with mock.patch.object(m, "resolve_binary", return_value=None): + with self.assertRaises(ValueError) as caught: + m.tool_project_info(str(z)) + self.assertIn("QET_BINARY", str(caught.exception)) + + +@needs_examples +class ZippedProjects(unittest.TestCase): + """Every tool reads and writes QElectroTech's zipped project (.qetz) as + it does the .qet: the XML tools read the .qet QElectroTech saves from + it, the tools that launch QElectroTech hand it the .qetz.""" + + @classmethod + def setUpClass(cls): + cls.tmp = tempfile.TemporaryDirectory() + root = Path(cls.tmp.name) + cls.env = mock.patch.dict(os.environ, {"QET_BINARY": BINARY, "QET_ENABLE_SCRIPTING": "1", + "QET_MCP_SCRIPTS_DIR": str(root / "scripts"), + "QET_MCP_INFO_FILE": str(root / "none.json")}) + cls.env.start() + cls.qet, cls.qetz = root / "p.qet", root / "p.qetz" + source = str(Path(EXAMPLES) / "industrial.qet") + for out in (cls.qet, cls.qetz): + r = m._run_qet(BINARY, ["--resave", source, str(out)], timeout=600) + assert out.is_file(), r + assert cls.qetz.read_bytes()[:2] == b"PK" + + @classmethod + def tearDownClass(cls): + cls.env.stop() + cls.tmp.cleanup() + + def test_read_tools_answer_as_for_the_qet(self): + def strip(d): + return {k: v for k, v in d.items() if k not in ("path", "file")} + for tool in (m.tool_project_info, m.tool_elements, m.tool_conductors, m.tool_items): + self.assertEqual(strip(tool(str(self.qetz))), strip(tool(str(self.qet))), tool.__name__) + + def test_diff_finds_nothing_between_the_qet_and_the_qetz(self): + d = m.tool_diff(str(self.qet), str(self.qetz)) + self.assertTrue(d.get("identical") or not any( + v.get("added") or v.get("removed") or v.get("changed") + for v in d.values() if isinstance(v, dict)), d) + + def test_scan_counts_a_qetz(self): + folder = Path(self.tmp.name) / "scan" + folder.mkdir(exist_ok=True) + for f in (self.qet, self.qetz): + shutil.copy2(f, folder / f.name) + scan = m.tool_scan(str(folder), "conductor", "cable", recursive=False) + self.assertEqual(scan["files"], 2) + self.assertEqual(scan["unreadable"], []) + self.assertEqual(scan["per_file"][0]["conductor"], scan["per_file"][1]["conductor"]) + + def test_a_qetz_is_converted_once_while_it_does_not_change(self): + m._QETZ_COPIES.clear() + real = m._run_qet + with mock.patch.object(m, "_run_qet", side_effect=real) as run: + m.tool_project_info(str(self.qetz)) + m.tool_elements(str(self.qetz)) + self.assertEqual(run.call_count, 1) + + def test_query_reads_the_qetz_itself(self): + sql = "SELECT COUNT(*) AS n FROM element" + self.assertEqual(m.tool_query(BINARY, str(self.qetz), sql)["rows"], + m.tool_query(BINARY, str(self.qet), sql)["rows"]) + + def test_edit_writes_a_qetz(self): + first = m.tool_elements(str(self.qetz))["elements"][0] + out = Path(self.tmp.name) / "edited.qetz" + r = m.tool_edit(BINARY, str(self.qetz), + [{"op": "rotate_element", "folio": first["folio"] - 1, + "element": first["uuid"], "angle": 90}], + str(out), ELEMENTS or None) + self.assertTrue(r.get("ok"), r) + self.assertEqual(out.read_bytes()[:2], b"PK") + # The rotation is what the diff of the two .qetz finds + diff = json.dumps(m.tool_diff(str(self.qetz), str(out))) + self.assertIn(m._uuid_key(first["uuid"]) if hasattr(m, "_uuid_key") else first["uuid"], diff.lower()) + + if __name__ == "__main__": print(f"binary : {BINARY or '(none: integration tests skipped)'}") print(f"elements : {ELEMENTS or '(none)'}") diff --git a/sources/SearchAndReplace/ui/searchandreplacewidget.cpp b/sources/SearchAndReplace/ui/searchandreplacewidget.cpp index 3a0e656ef..929710662 100644 --- a/sources/SearchAndReplace/ui/searchandreplacewidget.cpp +++ b/sources/SearchAndReplace/ui/searchandreplacewidget.cpp @@ -307,7 +307,7 @@ void SearchAndReplaceWidget::fillItemsList() m_indi_text_qtwi->sortChildren(0, Qt::AscendingOrder); - for (Conductor *c : dc.m_potential_conductors) + for (Conductor *c : dc.potentialConductors()) { QTreeWidgetItem *qtwi = new QTreeWidgetItem(m_conductor_qtwi); qtwi->setText(0, c->properties().text); diff --git a/sources/TerminalStrip/terminalstrip.cpp b/sources/TerminalStrip/terminalstrip.cpp index 337f56766..5bbcf5cee 100644 --- a/sources/TerminalStrip/terminalstrip.cpp +++ b/sources/TerminalStrip/terminalstrip.cpp @@ -1011,7 +1011,7 @@ 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): + //The folios holding its terminals are built (lazy folios): //a strip is read with the project, before the database could //tell where any symbol is QSet terminals; @@ -1020,8 +1020,9 @@ bool TerminalStrip::fromXml(QDomElement &xml_element) 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); + //Get all free elements terminal of the project: its own are on + //the folios just built + const ElementProvider ep(m_project->builtFolios()); auto free_terminals = ep.freeTerminal(); //Read each physical terminal diff --git a/sources/cli_export.cpp b/sources/cli_export.cpp index ffad87a83..5c4d5188f 100644 --- a/sources/cli_export.cpp +++ b/sources/cli_export.cpp @@ -873,16 +873,15 @@ int exportLinks(QETProject &project, const QString &output) int resaveProject(QETProject &project, const QString &output) { if (QETProject::isContainerPath(output)) { - const QDomDocument doc = project.toXml(); QString error; - if (!QETProject::writeContainer(doc, output, &error)) { + if (!project.writeContainerFile(output, &error)) { err << "Cannot write '" << output << "': " << error << "\n"; return 1; } out << "Re-saved project -> " << output << "\n"; return 0; } - //Folios not built (QET_LAZY_FOLIOS) written as read when that is + //Folios not built (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); @@ -971,15 +970,14 @@ int setTitleBlock(QETProject &project, const QString &output, } if (QETProject::isContainerPath(output)) { - const QDomDocument doc = project.toXml(); QString error; - if (!QETProject::writeContainer(doc, output, &error)) { + if (!project.writeContainerFile(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()) + //Folios not built (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); diff --git a/sources/conductornumexport.cpp b/sources/conductornumexport.cpp index f969a2015..ea43be511 100644 --- a/sources/conductornumexport.cpp +++ b/sources/conductornumexport.cpp @@ -19,11 +19,12 @@ #include "qetproject.h" #include "qetapp.h" #include "diagram.h" -#include "diagramcontent.h" #include "qetgraphicsitem/conductor.h" #include "qetgraphicsitem/conductortextitem.h" #include "qetgraphicsitem/element.h" #include "qetgraphicsitem/terminal.h" +#include "dataBase/projectdatabase.h" +#include "properties/elementdata.h" #include @@ -114,10 +115,30 @@ void ConductorNumExport::fillHash() { //We used this rx to avoid insert num composed only withe white space. QRegularExpression rx("^ *$"); - for (Diagram *d : m_project->diagrams()) + auto count = [this](const QString &num) { + m_hash.insert(num, m_hash.value(num, 0) + 1); + }; + //The folios not built (lazy folios): their wires from the + //project database, by the same rules + QList unbuilt; + for (Diagram *d : m_project->folios()) + if (!d->isLoaded()) unbuilt << d->uuid(); + for (const auto &wire : m_project->dataBase()->wiresOnFolios(unbuilt)) { - DiagramContent dc(d, false); - for (Conductor *c : dc.conductors()) + if (wire.text.isEmpty() || wire.text.contains(rx)) { + continue; + } + const bool report1 = ElementData::typeFromString(wire.kind1) & ElementData::AllReport; + const bool report2 = ElementData::typeFromString(wire.kind2) & ElementData::AllReport; + if (!(report1 && report2)) { + count(wire.text); + } + } + for (Diagram *d : m_project->builtFolios()) + { + //Its wires only: a DiagramContent would follow their + //potentials onto other folios, building those not built + for (Conductor *c : d->conductors()) { QString num = c->textItem()->toPlainText(); if (num.isEmpty() || num.contains(rx)) { diff --git a/sources/container/QETZ-FORMAT.md b/sources/container/QETZ-FORMAT.md index d7b2f0234..a88c84d36 100644 --- a/sources/container/QETZ-FORMAT.md +++ b/sources/container/QETZ-FORMAT.md @@ -1,8 +1,8 @@ -# The `.qetz` project file, format 3 +# The `.qetz` project file, format 4 A `.qetz` is a QElectroTech project saved as a zip archive: the same project a `.qet` holds, split into files, with the engineering data in an SQLite -database. This document describes format 3. `tst_qetcontainer` checks that +database. This document describes format 4. `tst_qetcontainer` checks that the schema below is the one QElectroTech writes. The code is `qetcontainer.cpp` (the zip and its files), `qetcontainerdb.cpp` @@ -29,15 +29,25 @@ The code is `qetcontainer.cpp` (the zip and its files), `qetcontainerdb.cpp` | Terminal strip | its terminals, their order and bridges | the strip drawn on a folio | Cables are kept in `project.xml` in this format. +- **Nothing computed is kept.** What QElectroTech works out again when it + builds a folio is left out of the folio file: a wire's description of its + ends (`element1_label`, `element1_name`, `element1_linked`, + `terminalname1`, and the same for end 2), the text of a symbol's text + item when it shows the symbol's information or a composition of it, and, + when every folio, symbol and wire has a uuid of its own, a symbol's list + of its terminals. - **Nothing is lost.** Anything the database does not model stays in the XML as it was. A block is moved only if it can be rebuilt exactly. +- **The database holds the project's data only.** QElectroTech's own + working database (search, numbering, cross-references) is built from it + when the project opens; it is not stored. ## 2. The archive | Entry | Content | |---|---| | `mimetype` | `application/x-qelectrotech-project+zip`, the first entry, stored uncompressed | -| `manifest.xml` | `` | +| `manifest.xml` | `` | | `project.xml` | the project document, each part replaced by `` | | `project.sqlite` | the engineering data (section 4) | | `folios/.xml` | one `` per folio, named by the folio's uuid (or its position when it has none) | @@ -79,12 +89,19 @@ The database repeats the format in `meta` (key `format`) and in | Format | Written by | Reading | |---|---|---| | 2 | QElectroTech 0.200.1 development builds (#1462) | read: rows keyed by position (`f1`, `e1`, `c1`) where an item had no uuid of its own, every value as text | -| 3 | this document | read and written | +| 3 | #1473 | read: as format 4, but with keys and constraints on its tables, and the computed text left in the folio files | +| 4 | this document | read and written | ## 4. The database ### 4.1 Schema +The tables are written **without keys or constraints**: each has the +columns and types below, but no `PRIMARY KEY`, `NOT NULL`, `UNIQUE` or +`REFERENCES`. With text uuids, keys and their indexes were half the file. A +reader builds the indexes it needs once the file is read, and checks itself +what the keys would have (section 5). + ```sql CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT); @@ -176,7 +193,7 @@ A `.qetz` is untrusted. A reader: plain table (a view, say); - refuses the file, with a message saying what is wrong, if the database and the folio files disagree: an item whose row is missing or on another - folio, an item twice, a row no item has, or information, links or - properties of a symbol or folio that does not exist. + folio, an item twice, a row no item has or a row twice, or information, + links or properties of a symbol or folio that does not exist. A damaged archive is caught by the zip format's own checksums. diff --git a/sources/container/qetcontainer.cpp b/sources/container/qetcontainer.cpp index 73a5f65d2..f6659ebbc 100644 --- a/sources/container/qetcontainer.cpp +++ b/sources/container/qetcontainer.cpp @@ -22,6 +22,8 @@ #include #include #include +#include +#include const char QetContainer::MimeType[] = "application/x-qelectrotech-project+zip"; @@ -93,6 +95,20 @@ QList childElements(const QDomElement &parent, const QString &tag) return list; } + //An element and the text nodes on either side of it out of its parent, + //one text node left where they were, so nothing is read twice +void removeElement(QDomElement e) +{ + QDomNode parent = e.parentNode(); + QDomNode before = e.previousSibling(), after = e.nextSibling(); + parent.removeChild(e); + if (before.isText() && after.isText()) { + before.setNodeValue(before.nodeValue() + after.nodeValue()); + parent.removeChild(after); + } +} + + //The collection's symbol definitions, each to elements// void splitCollection(QDomDocument &document, const QDomElement &category, const QString &path, QList *parts, QSet *taken) @@ -164,7 +180,7 @@ bool QetContainer::readDocument(const QByteArray &content, QDomDocument *documen return parse(content, document, error); QList entries; return QetZip::fromBytes(content, &entries, error) - && join(entries, document, error, pictures, format); + && join(entries, document, error, nullptr, pictures, format); } QString QetContainer::pictureFile(const QDomElement &image) @@ -172,6 +188,76 @@ QString QetContainer::pictureFile(const QDomElement &image) return image.attribute(ImageFileAttribute); } +/** + @brief QetContainer::ownUuids + @return true if every folio, symbol and wire of @p project has a uuid + no other has: a wire then finds its ends without the symbols' terminal + lists, which split() leaves out +*/ +bool QetContainer::ownUuids(const QDomElement &project) +{ + QSet seen; + auto own = [&seen](const QDomElement &item) { + const QString uuid = item.attribute(QStringLiteral("uuid")); + if (QUuid(uuid).isNull() || seen.contains(uuid)) return false; + seen.insert(uuid); + return true; + }; + for (const QDomElement &diagram : childElements(project, QStringLiteral("diagram"))) { + if (!own(diagram)) return false; + for (const QDomElement &list : childElements(diagram, QStringLiteral("elements"))) + for (const QDomElement &element : childElements(list, QStringLiteral("element"))) + if (!own(element)) return false; + for (const QDomElement &list : childElements(diagram, QStringLiteral("conductors"))) + for (const QDomElement &wire : childElements(list, QStringLiteral("conductor"))) + if (!own(wire)) return false; + } + return true; +} + +/** + @brief QetContainer::dropComputed + Take out of the folio @p diagram what QElectroTech computes when it + builds the folio (stage D3c): a wire's description of + its ends (element1_label, element1_name, element1_linked, terminalname1, + and the same for end 2), a symbol text's content where it shows the + symbol's information or a composition of it, and -- with + @p terminal_lists, when every symbol has a uuid of its own -- a symbol's + list of its terminals. Checked on 342 projects: opened and saved, each + comes back as it was, but the terminal lists of a file whose symbols + share a uuid (F100), where wires find their ends by them. +*/ +void QetContainer::dropComputed(QDomElement diagram, bool terminal_lists) +{ + for (QDomElement list : childElements(diagram, QStringLiteral("conductors"))) + for (QDomElement wire : childElements(list, QStringLiteral("conductor"))) + for (const char *end : {"1", "2"}) + for (const char *name : {"element%1_label", "element%1_name", "element%1_linked", + "terminalname%1"}) + wire.removeAttribute(QString::fromLatin1(name).arg(QLatin1String(end))); + for (QDomElement list : childElements(diagram, QStringLiteral("elements"))) + { + for (QDomElement element : childElements(list, QStringLiteral("element"))) + { + if (terminal_lists) + for (QDomElement terminals : childElements(element, QStringLiteral("terminals"))) + removeElement(terminals); + for (QDomElement texts : childElements(element, QStringLiteral("dynamic_texts"))) + { + for (QDomElement text : childElements(texts, QStringLiteral("dynamic_elmt_text"))) + { + const QString from = text.attribute(QStringLiteral("text_from")); + if (from != QLatin1String("ElementInfo") && from != QLatin1String("CompositeText")) + continue; + for (QDomElement content : childElements(text, QStringLiteral("text"))) + while (content.hasChildNodes()) + content.removeChild(content.firstChild()); + } + } + } + } +} + /** @brief QetContainer::split @param project : the project document, as QETProject::toXml() makes it @@ -180,7 +266,7 @@ QString QetContainer::pictureFile(const QDomElement &image) set if @p project is not a project */ QList QetContainer::split(const QDomDocument &project, const QString &writer, - QString *error) + QString *error, const QHash &kept_from) { QDomDocument document = project.cloneNode(true).toDocument(); QDomElement root = document.documentElement(); @@ -190,7 +276,34 @@ QList QetContainer::split(const QDomDocument &project, const QStr } //The engineering data out of the document, into project.sqlite - const QByteArray database = QetContainerDb::extract(root, error); + //A folio written as it was read (, + //stage D4) keeps its part and, through + //@p kept_from's project.sqlite, its rows + //A symbol changed on a folio kept as it was gets its information + //rows written anew: its part must have the place they go back to + for (const QDomElement &diagram : childElements(root, QStringLiteral("diagram"))) + { + const QList edited = childElements(diagram, QStringLiteral("qetz-edited-element")); + if (edited.isEmpty()) continue; + QDomDocument part; + if (!parse(kept_from.value(diagram.attribute(QStringLiteral("qetz-kept-part"))), &part, error)) + return {}; + QSet placed; + const QDomNodeList elements = part.elementsByTagName(QStringLiteral("element")); + for (int i = 0 ; i < elements.size() ; ++i) { + const QDomElement element = elements.at(i).toElement(); + for (const QDomElement &marker : childElements(element, QStringLiteral("qetz-db"))) + if (marker.attribute(QStringLiteral("block")) == QLatin1String("elementInformations")) + placed.insert(element.attribute(QStringLiteral("uuid"))); + } + for (const QDomElement &symbol : edited) + if (!placed.contains(symbol.attribute(QStringLiteral("uuid")))) { + setError(error, QStringLiteral("a symbol changed on a folio kept as it was has no place for its information")); + return {}; + } + } + const QByteArray database = QetContainerDb::extract( + root, error, kept_from.value(QStringLiteral("project.sqlite"))); if (database.isEmpty()) return {}; @@ -199,7 +312,36 @@ QList QetContainer::split(const QDomDocument &project, const QStr //Folios, each with its pictures taken out int folio = 0; + //Nothing QElectroTech computes is kept (format 4) + const bool own_uuids = ownUuids(root); + for (QDomElement diagram : childElements(root, QStringLiteral("diagram"))) + dropComputed(diagram, own_uuids); + + //The parts of the folios kept as they were, and their pictures QMap pictures; //name -> png, once each + for (QDomElement diagram : childElements(root, QStringLiteral("diagram"))) + { + const QString kept = diagram.attribute(QStringLiteral("qetz-kept-part")); + if (kept.isEmpty()) continue; + if (!kept_from.contains(kept)) { + setError(error, QStringLiteral("the part \"%1\" of a folio kept as it was is missing").arg(kept)); + return {}; + } + taken.insert(kept); + const QByteArray part = kept_from.value(kept); + static const QRegularExpression image_file(QStringLiteral("%1=\"(images/[^\"]+)\"").arg(ImageFileAttribute)); + for (const auto &match : image_file.globalMatch(QString::fromUtf8(part))) { + const QString name = match.captured(1); + if (!kept_from.contains(name)) { + setError(error, QStringLiteral("the picture \"%1\" of a folio kept as it was is missing").arg(name)); + return {}; + } + pictures.insert(name, kept_from.value(name)); + } + parts.append(QetZip::Entry{kept, part, true}); + root.replaceChild(placeholder(document, kept), diagram); + } + for (QDomElement diagram : childElements(root, QStringLiteral("diagram"))) { ++folio; @@ -273,12 +415,12 @@ QList QetContainer::split(const QDomDocument &project, const QStr (the manifest's min-reader), or a part missing or unreadable. Entries nothing names are ignored, so a later format may add some. With @p pictures, the pictures stay out of the document: see the - namespace. - @p format, if any: the format the file says it is, above Format for a - later one this may read (its min-reader allows it) + namespace. With @p folio_files, the part each folio was read from, in + order. @p format, if any: the format the file says it is, above Format + for a later one this may read (its min-reader allows it). */ bool QetContainer::join(const QList &entries, QDomDocument *project, QString *error, - Pictures *pictures, int *format) + QStringList *folio_files, Pictures *pictures, int *format) { QHash files; for (const QetZip::Entry &entry : entries) @@ -312,6 +454,9 @@ bool QetContainer::join(const QList &entries, QDomDocument *proje for (QDomElement part : placeholders) { const QString file = part.attribute(FileAttribute); + if (folio_files && part.parentNode() == project->documentElement() + && file.startsWith(QLatin1String("folios/"))) + folio_files->append(file); if (!files.contains(file)) { setError(error, QStringLiteral("part \"%1\" is missing").arg(file)); return false; diff --git a/sources/container/qetcontainer.h b/sources/container/qetcontainer.h index f53e9ab79..c5a22987a 100644 --- a/sources/container/qetcontainer.h +++ b/sources/container/qetcontainer.h @@ -57,30 +57,39 @@ namespace QetContainer using Pictures = QHash; /// The format written; files of a later format are refused by join() /// unless their min-reader allows this one. - constexpr int Format = 3; + constexpr int Format = 4; /// 3: the database's rows keyed by the items' uuids, true/false and /// show flags as 1/0, its PRAGMA user_version the format, the symbols' type left - /// in the folio files, no column for where a block sat. Format 2 is - /// still read; a later format whose min-reader allows this one is - /// read too, and said so through join()'s @p format. + /// in the folio files, no column for where a block sat. + /// 4: what QElectroTech computes left out of the folios + /// (dropComputed()), the tables written without keys or indexes. + /// project.sqlite holds the engineering data once: QElectroTech's + /// own project database is built from it when the project opens. + /// Formats 2 and 3 are still read; a later format whose min-reader + /// allows this one is read too, and said so through join()'s + /// @p format. QList split(const QDomDocument &project, const QString &writer = QString(), - QString *error = nullptr); + QString *error = nullptr, + const QHash &kept_from = QHash()); bool join(const QList &entries, QDomDocument *project, - QString *error = nullptr, Pictures *pictures = nullptr, - int *format = nullptr); + QString *error = nullptr, QStringList *folio_files = nullptr, + Pictures *pictures = nullptr, int *format = nullptr); /// The file join() left the picture of @p image in, or an empty string QString pictureFile(const QDomElement &image); + /// Take what QElectroTech computes out of a folio (format 4) + void dropComputed(QDomElement diagram, bool terminal_lists); + /// true if every folio, symbol and wire has a uuid of its own + bool ownUuids(const QDomElement &project); /// Parse @p xml as a .qet is read (QETProject::openFile) bool parse(const QByteArray &xml, QDomDocument *document, QString *error = nullptr); /// true if @p content is a zip, so a .qetz rather than a .qet bool isZip(const QByteArray &content); /// The project document in @p content, a .qet or a .qetz bool readDocument(const QByteArray &content, QDomDocument *document, - QString *error = nullptr, Pictures *pictures = nullptr, - int *format = nullptr); + QString *error = nullptr, Pictures *pictures = nullptr, int *format = nullptr); } #endif // QETCONTAINER_H diff --git a/sources/container/qetcontainerdb.cpp b/sources/container/qetcontainerdb.cpp index 66cf9cbf8..70d03bd07 100644 --- a/sources/container/qetcontainerdb.cpp +++ b/sources/container/qetcontainerdb.cpp @@ -33,6 +33,8 @@ namespace { const QString KeyAttribute = QStringLiteral("qetz-key"); const QString FolioAttribute = QStringLiteral("qetz-folio"); + //A folio written as it was read (stage D4): its part +const QString KeptPartAttribute = QStringLiteral("qetz-kept-part"); //What a moved block leaves where it was. Removing it instead would leave //the whitespace before and after it side by side, read back as one text //node: the parent would come back a child short. @@ -145,28 +147,53 @@ QString schema() .arg(columns(FolioData), columns(ElementData), columns(ConductorData)); } - //Owns an sqlite3 handle + //The tables of a document's engineering data +const QStringList DocTables = { + QStringLiteral("strip_bridge_terminal"), QStringLiteral("strip_bridge"), + QStringLiteral("strip_terminal"), QStringLiteral("terminal_strip"), + QStringLiteral("project_property"), QStringLiteral("folio_property"), QStringLiteral("folio"), + QStringLiteral("element_info"), QStringLiteral("element"), QStringLiteral("link"), + QStringLiteral("conductor"), QStringLiteral("meta")}; + + //Owns an sqlite3 handle; names its columns as the format of the + //database read names them class Database { public: ~Database() { if (m_db) sqlite3_close(m_db); } sqlite3 *m_db = nullptr; + int m_format = QetContainer::Format; bool exec(const QString &sql) { return sqlite3_exec(m_db, sql.toUtf8().constData(), nullptr, nullptr, nullptr) == SQLITE_OK; } QString error() const { return QString::fromUtf8(sqlite3_errmsg(m_db)); } + //Format 2 (QElectroTech 0.200.1 development builds) keyed its + //rows apart from the items' uuids: "f1", "e1", "c1" where an item + //had none of its own, and every item's key in the folio file + bool v2() const { return m_format == 2; } + QString folioKey() const { return v2() ? QStringLiteral("id") : QStringLiteral("uuid"); } + QString folioRef() const { return v2() ? QStringLiteral("folio_id") : QStringLiteral("folio_uuid"); } + QString itemKey() const { return v2() ? QStringLiteral("key") : QStringLiteral("uuid"); } + QString elementRef() const { return v2() ? QStringLiteral("element_key") : QStringLiteral("element_uuid"); } + QString showColumn() const { return v2() ? QStringLiteral("NULL") : QStringLiteral("show"); } + QStringList elementData() const { return v2() ? ElementDataV2 : ElementData; } + QStringList conductorData() const { return v2() ? QStringList{QStringLiteral("uuid")} + ConductorData : ConductorData; } }; //Owns a prepared statement; binds and reads QVariants (null = SQL NULL) class Statement { public: - Statement(sqlite3 *db, const QString &sql) { - sqlite3_prepare_v2(db, sql.toUtf8().constData(), -1, &m_stmt, nullptr); + Statement(const Database &db, const QString &sql) { + sqlite3_prepare_v2(db.m_db, sql.toUtf8().constData(), -1, &m_stmt, nullptr); } ~Statement() { sqlite3_finalize(m_stmt); } bool ok() const { return m_stmt; } + //A statement that could not be prepared (the database is not + //one, a table is missing) binds nothing and steps to no row: + //SQLite's own calls are not all safe on a null statement void bind(const QVariantList &values) { + if (!m_stmt) return; sqlite3_reset(m_stmt); sqlite3_clear_bindings(m_stmt); for (int i = 0 ; i < values.size() ; ++i) { @@ -182,11 +209,13 @@ class Statement } } bool run(const QVariantList &values) { + if (!m_stmt) return false; bind(values); return sqlite3_step(m_stmt) == SQLITE_DONE; } - bool next() { return sqlite3_step(m_stmt) == SQLITE_ROW; } + bool next() { return m_stmt && sqlite3_step(m_stmt) == SQLITE_ROW; } QVariant value(int column) const { + if (!m_stmt) return QVariant(); switch (sqlite3_column_type(m_stmt, column)) { case SQLITE_NULL: return QVariant(); case SQLITE_INTEGER: return QVariant(sqlite3_column_int(m_stmt, column)); @@ -471,22 +500,26 @@ QVariantList entryRow(const QDomElement &e, const QString &usual_tag) } //The key of each of @p items: its uuid, when it has one no other of - //@p items has, so the uuid the folio file already holds says which row - //is the item's (@p own true). Else (F100: symbols copied with their - //uuid) a uuid made from the item itself -- the same on every save, - //never from where the item is -- kept in the folio file as qetz-key. -QStringList keys(const QList &items, QList *own) + //@p items has and @p reserved does not hold, so the uuid the folio file + //already holds says which row is the item's (@p own true). Else (F100: + //symbols copied with their uuid) a uuid made from the item itself -- + //the same on every save, never from where the item is -- kept in the + //folio file as qetz-key. @p reserved: the keys of rows copied from + //another database (a folio kept as it was) +QStringList keys(const QList &items, QList *own, + const QSet &reserved = QSet()) { static const QUuid key_namespace(QStringLiteral("{3f0c6c1e-5b8e-4b8a-9d55-6b2f5f0f9a41}")); QHash seen; for (const QDomElement &e : items) if (!QUuid(e.attribute(QStringLiteral("uuid"))).isNull()) ++seen[e.attribute(QStringLiteral("uuid"))]; QSet taken(seen.keyBegin(), seen.keyEnd()); + taken.unite(reserved); QHash twins; QStringList out; for (const QDomElement &e : items) { const QString uuid = e.attribute(QStringLiteral("uuid")); - if (seen.value(uuid) == 1) { + if (seen.value(uuid) == 1 && !reserved.contains(uuid)) { out << uuid; *own << true; continue; @@ -507,6 +540,287 @@ QStringList keys(const QList &items, QList *own) } return out; } + + //Every table of @p db written again without its keys and indexes, in + //the order of its rows, and the file compacted: the file a .qetz holds + //is read whole, its lookups indexed once read (openDatabase()), and keys + //and indexes of text uuids were half of it (stage D3b) +bool plainTables(sqlite3 *db) +{ + QStringList tables; + { + sqlite3_stmt *list = nullptr; + sqlite3_prepare_v2(db, "SELECT name FROM main.sqlite_master WHERE type = 'table' " + "AND name NOT LIKE 'sqlite_%' ORDER BY name", -1, &list, nullptr); + while (list && sqlite3_step(list) == SQLITE_ROW) + tables << QString::fromUtf8(reinterpret_cast(sqlite3_column_text(list, 0))); + sqlite3_finalize(list); + } + auto run = [db](const QString &sql) { + return sqlite3_exec(db, sql.toUtf8().constData(), nullptr, nullptr, nullptr) == SQLITE_OK; + }; + bool ok = run(QStringLiteral("BEGIN")); + for (const QString &table : std::as_const(tables)) { + const QString plain = table + QStringLiteral("__plain"); + //The same columns and types (QETZ-FORMAT.md), no key or constraint + QStringList columns; + { + sqlite3_stmt *info = nullptr; + sqlite3_prepare_v2(db, QStringLiteral("SELECT name, type FROM pragma_table_info('%1') ORDER BY cid") + .arg(table).toUtf8().constData(), -1, &info, nullptr); + while (info && sqlite3_step(info) == SQLITE_ROW) + columns << QStringLiteral("\"%1\" %2").arg( + QString::fromUtf8(reinterpret_cast(sqlite3_column_text(info, 0))), + QString::fromUtf8(reinterpret_cast(sqlite3_column_text(info, 1)))); + sqlite3_finalize(info); + } + ok = ok && !columns.isEmpty() + && run(QStringLiteral("CREATE TABLE \"%1\" (%2)").arg(plain, columns.join(QStringLiteral(", ")))) + && run(QStringLiteral("INSERT INTO \"%1\" SELECT * FROM \"%2\" ORDER BY rowid").arg(plain, table)) + && run(QStringLiteral("DROP TABLE \"%1\"").arg(table)) + && run(QStringLiteral("ALTER TABLE \"%1\" RENAME TO \"%2\"").arg(plain, table)); + } + ok = ok && run(QStringLiteral("COMMIT")); + return ok && run(QStringLiteral("VACUUM")); +} + + //@p database opened in @p db, its tables checked to be plain tables and + //its format one this reads: a file is untrusted +bool openDatabase(Database &db, const QByteArray &database, QString *error) +{ + if (sqlite3_open(":memory:", &db.m_db) != SQLITE_OK) { + setError(error, QStringLiteral("cannot open the project database")); + return false; + } + unsigned char *copy = static_cast(sqlite3_malloc64(sqlite3_uint64(database.size()))); + if (!copy) { setError(error, QStringLiteral("out of memory")); return false; } + memcpy(copy, database.constData(), size_t(database.size())); + //A copy of its own, writable: its lookups are indexed below + if (sqlite3_deserialize(db.m_db, "main", copy, database.size(), database.size(), + SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_RESIZEABLE) != SQLITE_OK) { + setError(error, QStringLiteral("the project database cannot be read")); + return false; + } + //A file is untrusted: nothing in its schema may run, and every name + //read must be a plain table. + db.exec(QStringLiteral("PRAGMA trusted_schema = OFF")); + for (const QString &table : DocTables) { + Statement kind(db, QStringLiteral("SELECT type FROM sqlite_master WHERE name = ?")); + kind.bind({table}); + if (!kind.next() || kind.value(0).toString() != QLatin1String("table")) { + setError(error, QStringLiteral("the project database has no table %1").arg(table)); + return false; + } + } + { + Statement format(db, QStringLiteral("SELECT value FROM meta WHERE key = 'format'")); + const QString text = format.next() ? format.value(0).toString() : QString(); + db.m_format = text.toInt(); + //A later format is read when the manifest's min-reader allows it + //(QetContainer::join()): its writer kept what this reads + if (db.m_format < 2) { + setError(error, QStringLiteral("the project database is of format %1, this reads format 2 on") + .arg(text)); + return false; + } + } + //What the folios are read by: a file holds no index (plainTables()) + const QStringList indexes = { + QStringLiteral("folio(%1)").arg(db.folioKey()), QStringLiteral("folio_property(%1)").arg(db.folioRef()), + QStringLiteral("element(%1)").arg(db.itemKey()), QStringLiteral("element(%1)").arg(db.folioRef()), + QStringLiteral("element_info(%1)").arg(db.elementRef()), QStringLiteral("link(%1)").arg(db.elementRef()), + QStringLiteral("conductor(%1)").arg(db.itemKey()), QStringLiteral("meta(key)")}; + for (const QString &on : indexes) { + const QString name = QStringLiteral("qetz_read_") + QString(on).replace(QLatin1Char('('), QLatin1Char('_')).remove(QLatin1Char(')')); + if (!db.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS %1 ON %2").arg(name, on))) { + setError(error, QStringLiteral("the project database cannot be read: %1").arg(db.error())); + return false; + } + } + db.exec(QStringLiteral("PRAGMA query_only = ON")); + return true; +} + +QDomElement entryElement(QDomDocument &document, const QVariant &name_attr, const QString &name, + const QVariant &value, const QVariant &attrs, const QString &default_tag, + const QVariant &show = QVariant()) +{ + const QJsonObject o = QJsonDocument::fromJson(attrs.toString().toUtf8()).object(); + QDomElement e = document.createElement(o.value(QStringLiteral("tag")).toString(default_tag)); + if (!name_attr.isNull()) e.setAttribute(name, name_attr.toString()); + if (!show.isNull()) e.setAttribute(QStringLiteral("show"), show.toString()); + setAttributesJson(e, attrs); + if (!value.isNull()) e.appendChild(document.createTextNode(value.toString())); + return e; +} + + //An item's key: the one it was given, else (format 3 on) its uuid +QString keyOf(const Database &db, QDomElement e, const QString &attribute) +{ + if (e.hasAttribute(attribute)) { + const QString key = e.attribute(attribute); + e.removeAttribute(attribute); + return key; + } + return db.v2() ? QString() : e.attribute(QStringLiteral("uuid")); +} + +QString quoted(const QStringList &names) +{ + return QStringLiteral("\"") + names.join(QStringLiteral("\", \"")) + QStringLiteral("\""); +} + + //The keys of the rows restore() read, each once: every row must be an + //item's, on the folio the item is on -- nothing in the file, which is + //untrusted, enforces it +struct UsedRows +{ + QSet folios, elements, conductors; + bool use(QSet &used, const QString &key, const QString &what, QString *error) { + if (used.contains(key)) { + setError(error, QStringLiteral("%1 %2 is in the folios twice").arg(what, key)); + return false; + } + used.insert(key); + return true; + } +}; + + //The engineering data of the folio @p d back from @p db +bool restoreDiagram(Database &db, QDomElement d, UsedRows &used, QString *error) +{ + QDomDocument document = d.ownerDocument(); + auto entry = [&document](const QVariant &name_attr, const QString &name, const QVariant &value, + const QVariant &attrs, const QString &default_tag, const QVariant &show = QVariant()) { + return entryElement(document, name_attr, name, value, attrs, default_tag, show); + }; + const QString id = keyOf(db, d, FolioAttribute); + if (id.isEmpty()) return true; + if (!used.use(used.folios, id, QStringLiteral("folio"), error)) return false; + Statement f(db, QStringLiteral("SELECT %1, props_attrs FROM folio WHERE %2 = ?") + .arg(quoted(FolioData), db.folioKey())); + f.bind({id}); + if (!f.next()) { setError(error, QStringLiteral("folio %1 is not in the database").arg(id)); return false; } + QVariantList values; + for (int i = 0 ; i < FolioData.size() + 1 ; ++i) values << f.value(i); + put(d, FolioData, values, 0); + //A moved block goes back where its marker is: the marker says it + //was there + if (hasMarker(d, QStringLiteral("properties"))) { + QDomElement props = document.createElement(QStringLiteral("properties")); + setAttributesJson(props, values.at(FolioData.size())); + Statement p(db, QStringLiteral("SELECT name, value, attrs, %1 FROM folio_property WHERE %2 = ? ORDER BY ord") + .arg(db.showColumn(), db.folioRef())); + p.bind({id}); + while (p.next()) + props.appendChild(entry(p.value(0), QStringLiteral("name"), p.value(1), p.value(2), + QStringLiteral("property"), p.value(3))); + restoreWhitespace(props, values.at(FolioData.size())); + if (!replaceMarker(d, props)) { + setError(error, QStringLiteral("folio %1's properties have no place").arg(id)); + return false; + } + } + + const QStringList element_data = db.elementData(); + const QStringList conductor_data = db.conductorData(); + for (const QDomElement &list : children(d, QStringLiteral("elements"))) { + for (QDomElement e : children(list, QStringLiteral("element"))) { + const QString key = keyOf(db, e, KeyAttribute); + if (key.isEmpty()) continue; + if (!used.use(used.elements, key, QStringLiteral("symbol"), error)) return false; + Statement s(db, QStringLiteral("SELECT %1, info_attrs, links_attrs, %2 FROM element WHERE %3 = ?") + .arg(quoted(element_data), db.folioRef(), db.itemKey())); + s.bind({key}); + if (!s.next()) { setError(error, QStringLiteral("symbol %1 is not in the database").arg(key)); return false; } + if (s.value(element_data.size() + 2).toString() != id) { + setError(error, QStringLiteral("symbol %1 is on folio %2, the database says another").arg(key, id)); + return false; + } + QVariantList v; + for (int i = 0 ; i < element_data.size() + 2 ; ++i) v << s.value(i); + put(e, element_data, v, 0); + const int n = element_data.size(); + QDomElement info, links; + if (hasMarker(e, QStringLiteral("elementInformations"))) { + info = document.createElement(QStringLiteral("elementInformations")); + setAttributesJson(info, v.at(n)); + Statement i(db, QStringLiteral("SELECT name, value, attrs, %1 FROM element_info WHERE %2 = ? ORDER BY ord") + .arg(db.showColumn(), db.elementRef())); + i.bind({key}); + while (i.next()) + info.appendChild(entry(i.value(0), QStringLiteral("name"), i.value(1), i.value(2), + QStringLiteral("elementInformation"), i.value(3))); + restoreWhitespace(info, v.at(n)); + } + if (hasMarker(e, QStringLiteral("links_uuids"))) { + links = document.createElement(QStringLiteral("links_uuids")); + setAttributesJson(links, v.at(n + 1)); + Statement l(db, QStringLiteral("SELECT linked_uuid, attrs FROM link WHERE %1 = ? ORDER BY ord") + .arg(db.elementRef())); + l.bind({key}); + while (l.next()) + links.appendChild(entry(l.value(0), QStringLiteral("uuid"), QVariant(), l.value(1), + QStringLiteral("link_uuid"))); + restoreWhitespace(links, v.at(n + 1)); + } + if ((!info.isNull() && !replaceMarker(e, info)) + || (!links.isNull() && !replaceMarker(e, links))) { + setError(error, QStringLiteral("symbol %1's information has no place").arg(key)); + return false; + } + } + } + for (const QDomElement &list : children(d, QStringLiteral("conductors"))) { + for (QDomElement c : children(list, QStringLiteral("conductor"))) { + const QString key = keyOf(db, c, KeyAttribute); + if (key.isEmpty()) continue; + if (!used.use(used.conductors, key, QStringLiteral("wire"), error)) return false; + Statement s(db, QStringLiteral("SELECT %1, %2 FROM conductor WHERE %3 = ?") + .arg(quoted(conductor_data), db.folioRef(), db.itemKey())); + s.bind({key}); + if (!s.next()) { setError(error, QStringLiteral("wire %1 is not in the database").arg(key)); return false; } + if (s.value(conductor_data.size()).toString() != id) { + setError(error, QStringLiteral("wire %1 is on folio %2, the database says another").arg(key, id)); + return false; + } + QVariantList v; + for (int i = 0 ; i < conductor_data.size() ; ++i) v << s.value(i); + put(c, conductor_data, v, 0); + } + } + return true; +} + + //No row of @p db left over once restoreDiagram() read every folio: + //each table's rows, counted, are the items read (@p used), and every + //entry belongs to a row +bool noRowLeft(const Database &db, const UsedRows &used, QString *error) +{ + auto count = [&db](const QString &sql) { + Statement s(db, sql); + return s.next() ? s.value(0).toInt() : -1; + }; + const struct { QString sql; int expected; QString what; } checks[] = { + {QStringLiteral("SELECT count(*) FROM folio"), int(used.folios.size()), QStringLiteral("folio rows")}, + {QStringLiteral("SELECT count(*) FROM element"), int(used.elements.size()), QStringLiteral("symbol rows")}, + {QStringLiteral("SELECT count(*) FROM conductor"), int(used.conductors.size()), QStringLiteral("wire rows")}, + {QStringLiteral("SELECT count(*) FROM folio_property WHERE %1 NOT IN (SELECT %2 FROM folio)") + .arg(db.folioRef(), db.folioKey()), 0, QStringLiteral("folio property rows of no folio")}, + {QStringLiteral("SELECT count(*) FROM element_info WHERE %1 NOT IN (SELECT %2 FROM element)") + .arg(db.elementRef(), db.itemKey()), 0, QStringLiteral("information rows of no symbol")}, + {QStringLiteral("SELECT count(*) FROM link WHERE %1 NOT IN (SELECT %2 FROM element)") + .arg(db.elementRef(), db.itemKey()), 0, QStringLiteral("link rows of no symbol")}}; + for (const auto &check : checks) { + const int found = count(check.sql); + if (found != check.expected) { + setError(error, QStringLiteral("the project database does not match its folios: %1 %2, %3 expected") + .arg(found).arg(check.what).arg(check.expected)); + return false; + } + } + return true; +} } /** @@ -515,23 +829,45 @@ QStringList keys(const QList &items, QList *own) folios still in it) into a new database. @return the database file, or an empty array with @p error set */ -QByteArray QetContainerDb::extract(QDomElement project, QString *error) +QByteArray QetContainerDb::extract(QDomElement project, QString *error, const QByteArray &kept_from) { Database db; if (sqlite3_open(":memory:", &db.m_db) != SQLITE_OK || !db.exec(schema())) { setError(error, QStringLiteral("cannot create the project database")); return {}; + } + //The database of the .qetz this project was read from, for the folios + //kept as they were (stage D4): of this format (currentFormat()) + if (!kept_from.isEmpty()) { + unsigned char *copy = static_cast(sqlite3_malloc64(sqlite3_uint64(kept_from.size()))); + if (!copy) { setError(error, QStringLiteral("out of memory")); return {}; } + memcpy(copy, kept_from.constData(), size_t(kept_from.size())); + if (!db.exec(QStringLiteral("ATTACH DATABASE ':memory:' AS old")) + || sqlite3_deserialize(db.m_db, "old", copy, kept_from.size(), kept_from.size(), + SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_READONLY) != SQLITE_OK) { + setError(error, QStringLiteral("cannot read the database the kept folios come from")); + return {}; + } + db.exec(QStringLiteral("PRAGMA trusted_schema = OFF")); + for (const QString &table : DocTables) { + Statement kind(db, QStringLiteral("SELECT type FROM old.sqlite_master WHERE name = ?")); + kind.bind({table}); + if (!kind.next() || kind.value(0).toString() != QLatin1String("table")) { + setError(error, QStringLiteral("the kept folios' database has no table %1").arg(table)); + return {}; + } + } } db.exec(QStringLiteral("BEGIN")); - Statement meta(db.m_db, QStringLiteral("INSERT INTO meta VALUES (?,?)")); - Statement project_property(db.m_db, QStringLiteral("INSERT INTO project_property VALUES (?,?,?,?,?)")); - Statement folio(db.m_db, QStringLiteral("INSERT INTO folio VALUES (?,?,%1,?)").arg(placeholders(FolioData.size()))); - Statement folio_property(db.m_db, QStringLiteral("INSERT INTO folio_property VALUES (?,?,?,?,?,?)")); - Statement element(db.m_db, QStringLiteral("INSERT INTO element VALUES (?,?,%1,?,?)").arg(placeholders(ElementData.size()))); - Statement element_info(db.m_db, QStringLiteral("INSERT INTO element_info VALUES (?,?,?,?,?,?)")); - Statement link(db.m_db, QStringLiteral("INSERT INTO link VALUES (?,?,?,?)")); - Statement conductor(db.m_db, QStringLiteral("INSERT INTO conductor VALUES (?,?,%1)").arg(placeholders(ConductorData.size()))); + Statement meta(db, QStringLiteral("INSERT INTO meta VALUES (?,?)")); + Statement project_property(db, QStringLiteral("INSERT INTO project_property VALUES (?,?,?,?,?)")); + Statement folio(db, QStringLiteral("INSERT INTO folio VALUES (?,?,%1,?)").arg(placeholders(FolioData.size()))); + Statement folio_property(db, QStringLiteral("INSERT INTO folio_property VALUES (?,?,?,?,?,?)")); + Statement element(db, QStringLiteral("INSERT INTO element VALUES (?,?,%1,?,?)").arg(placeholders(ElementData.size()))); + Statement element_info(db, QStringLiteral("INSERT INTO element_info VALUES (?,?,?,?,?,?)")); + Statement link(db, QStringLiteral("INSERT INTO link VALUES (?,?,?,?)")); + Statement conductor(db, QStringLiteral("INSERT INTO conductor VALUES (?,?,%1)").arg(placeholders(ConductorData.size()))); bool ok = meta.ok() && project_property.ok() && folio.ok() && folio_property.ok() && element.ok() && element_info.ok() && link.ok() && conductor.ok(); @@ -544,6 +880,13 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error) for (QDomElement p : children(block, QStringLiteral("property"))) ok = ok && rows.run(QVariantList(prefix) << ord++ << entryRow(p, QStringLiteral("property"))); }; + //The entries of @p block, as the rows of the + //symbol @p key + auto moveInformation = [&](const QDomElement &block, const QString &key) { + int ord = 0; + for (QDomElement i : children(block, QStringLiteral("elementInformation"))) + ok = ok && element_info.run(QVariantList{key, ord++} << entryRow(i, QStringLiteral("elementInformation"))); + }; //Project properties const QDomElement project_props = project.firstChildElement(QStringLiteral("properties")); @@ -560,11 +903,23 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error) all_elements += children(list, QStringLiteral("element")); for (const QDomElement &list : children(d, QStringLiteral("conductors"))) all_conductors += children(list, QStringLiteral("conductor")); + } + //The keys of the rows the folios kept as they were bring, which no + //other item may have + QSet kept_elements, kept_conductors; + for (const QDomElement &d : diagrams) { + if (!d.hasAttribute(KeptPartAttribute)) continue; + Statement e(db, QStringLiteral("SELECT uuid FROM old.element WHERE folio_uuid = ?")); + e.bind({d.attribute(QStringLiteral("uuid"))}); + while (e.next()) kept_elements.insert(e.value(0).toString()); + Statement c(db, QStringLiteral("SELECT uuid FROM old.conductor WHERE folio_uuid = ?")); + c.bind({d.attribute(QStringLiteral("uuid"))}); + while (c.next()) kept_conductors.insert(c.value(0).toString()); } QList folio_own, element_own, conductor_own; const QStringList folio_keys = keys(diagrams, &folio_own); - const QStringList element_keys = keys(all_elements, &element_own); - const QStringList conductor_keys = keys(all_conductors, &conductor_own); + const QStringList element_keys = keys(all_elements, &element_own, kept_elements); + const QStringList conductor_keys = keys(all_conductors, &conductor_own, kept_conductors); int e_index = 0, c_index = 0; int pos = 0; @@ -573,6 +928,64 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error) const QString id = folio_keys.at(pos); const bool own_id = folio_own.at(pos); ++pos; + //Kept as it was: its rows from the database it was read from, + //under its uuid + if (d.hasAttribute(KeptPartAttribute)) { + if (kept_from.isEmpty() || !own_id) { + setError(error, QStringLiteral("a folio kept as it was, without its database")); + return {}; + } + Statement copy(db, QStringLiteral("INSERT INTO folio SELECT uuid, ?, %1, props_attrs " + "FROM old.folio WHERE uuid = ?").arg(quoted(FolioData))); + ok = ok && copy.run({pos, id}) && sqlite3_changes(db.m_db) == 1; + for (const QString &sql : { + QStringLiteral("INSERT INTO folio_property SELECT * FROM old.folio_property WHERE folio_uuid = ? ORDER BY rowid"), + QStringLiteral("INSERT INTO element SELECT * FROM old.element WHERE folio_uuid = ? ORDER BY rowid"), + QStringLiteral("INSERT INTO element_info SELECT * FROM old.element_info WHERE element_uuid IN " + "(SELECT uuid FROM old.element WHERE folio_uuid = ?) ORDER BY rowid"), + QStringLiteral("INSERT INTO link SELECT * FROM old.link WHERE element_uuid IN " + "(SELECT uuid FROM old.element WHERE folio_uuid = ?) ORDER BY rowid"), + QStringLiteral("INSERT INTO conductor SELECT * FROM old.conductor WHERE folio_uuid = ? ORDER BY rowid")}) { + Statement rows(db, sql); + ok = ok && rows.run({id}); + } + //Its symbols whose information changed since (QETProject::toXml()): + //their information rows written as a symbol's block writes them + for (const QDomElement &edited : children(d, QStringLiteral("qetz-edited-element"))) { + //Its part has the place they go back to (QetContainer::split()) + Statement find(db, QStringLiteral("SELECT uuid FROM element WHERE folio_uuid = ? AND uuid = ?")); + find.bind({id, edited.attribute(QStringLiteral("uuid"))}); + if (!find.next()) { + setError(error, QStringLiteral("a symbol changed on folio %1 kept as it was is not in its database").arg(id)); + return {}; + } + const QString key = find.value(0).toString(); + Statement drop(db, QStringLiteral("DELETE FROM element_info WHERE element_uuid = ?")); + ok = ok && drop.run({key}); + moveInformation(edited.firstChildElement(QStringLiteral("elementInformations")), key); + } + //Its wires whose properties changed since: their columns + for (const QDomElement &edited : children(d, QStringLiteral("qetz-edited-conductor"))) { + QStringList set; + QVariantList values; + for (const QString &column : ConductorData) { + if (column.startsWith(QLatin1String("element")) || column.startsWith(QLatin1String("terminal")) + || BooleanData.contains(column) || !edited.hasAttribute(column)) + continue; + set << QStringLiteral("\"%1\" = ?").arg(column); + values << edited.attribute(column); + } + values << id << edited.attribute(QStringLiteral("uuid")); + Statement update(db, QStringLiteral("UPDATE conductor SET %1 WHERE folio_uuid = ? AND uuid = ?") + .arg(set.join(QStringLiteral(", ")))); + ok = ok && !set.isEmpty() && update.run(values) && sqlite3_changes(db.m_db) == 1; + } + if (!ok) { + setError(error, QStringLiteral("folio %1 kept as it was is not in its database").arg(id)); + return {}; + } + continue; + } QVariantList row{id, pos}; row += take(d, FolioData); QDomElement props = d.firstChildElement(QStringLiteral("properties")); @@ -598,12 +1011,8 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error) erow << (info.isNull() ? QVariant() : blockJson(info)) << (links.isNull() ? QVariant() : blockJson(links)); ok = ok && element.run(erow); - if (!info.isNull()) { - int ord = 0; - for (QDomElement i : children(info, QStringLiteral("elementInformation"))) - ok = ok && element_info.run(QVariantList{key, ord++} - << entryRow(i, QStringLiteral("elementInformation"))); - } + if (!info.isNull()) + moveInformation(info, key); if (!links.isNull()) { int ord = 0; for (QDomElement l : children(links, QStringLiteral("link_uuid"))) @@ -632,10 +1041,10 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error) const QDomElement strips = project.firstChildElement(QStringLiteral("terminal_strips")); StripRows strip_rows; if (!strips.isNull() && stripRows(strips, &strip_rows)) { - Statement st(db.m_db, QStringLiteral("INSERT INTO terminal_strip VALUES (?,?,?,?,?,?,?)")); - Statement tt(db.m_db, QStringLiteral("INSERT INTO strip_terminal VALUES (?,?,?,?)")); - Statement bt(db.m_db, QStringLiteral("INSERT INTO strip_bridge VALUES (?,?,?,?)")); - Statement btt(db.m_db, QStringLiteral("INSERT INTO strip_bridge_terminal VALUES (?,?,?)")); + Statement st(db, QStringLiteral("INSERT INTO terminal_strip VALUES (?,?,?,?,?,?,?)")); + Statement tt(db, QStringLiteral("INSERT INTO strip_terminal VALUES (?,?,?,?)")); + Statement bt(db, QStringLiteral("INSERT INTO strip_bridge VALUES (?,?,?,?)")); + Statement btt(db, QStringLiteral("INSERT INTO strip_bridge_terminal VALUES (?,?,?)")); for (const QVariantList &r : strip_rows.strips) ok = ok && st.run(r); for (const QVariantList &r : strip_rows.terminals) ok = ok && tt.run(r); for (const QVariantList &r : strip_rows.bridges) ok = ok && bt.run(r); @@ -648,6 +1057,13 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error) setError(error, QStringLiteral("cannot fill the project database: %1").arg(db.error())); return {}; } + if (!kept_from.isEmpty()) { + sqlite3_exec(db.m_db, "DETACH DATABASE old", nullptr, nullptr, nullptr); + } + if (!plainTables(db.m_db)) { + setError(error, QStringLiteral("cannot write the project database: %1").arg(db.error())); + return {}; + } sqlite3_int64 size = 0; unsigned char *bytes = sqlite3_serialize(db.m_db, "main", &size, 0); @@ -669,91 +1085,23 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS { QDomDocument document = project.ownerDocument(); Database db; - if (sqlite3_open(":memory:", &db.m_db) != SQLITE_OK) { - setError(error, QStringLiteral("cannot open the project database")); + if (!openDatabase(db, database, error)) return false; - } - unsigned char *copy = static_cast(sqlite3_malloc64(sqlite3_uint64(database.size()))); - if (!copy) { setError(error, QStringLiteral("out of memory")); return false; } - memcpy(copy, database.constData(), size_t(database.size())); - if (sqlite3_deserialize(db.m_db, "main", copy, database.size(), database.size(), - SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_READONLY) != SQLITE_OK) { - setError(error, QStringLiteral("the project database cannot be read")); - return false; - } - //A file is untrusted: nothing in its schema may run, and every name - //read must be a plain table. - db.exec(QStringLiteral("PRAGMA trusted_schema = OFF")); - db.exec(QStringLiteral("PRAGMA query_only = ON")); - for (const char *table : {"meta", "project_property", "folio", "folio_property", - "element", "element_info", "link", "conductor", "terminal_strip", - "strip_terminal", "strip_bridge", "strip_bridge_terminal"}) { - Statement kind(db.m_db, QStringLiteral("SELECT type FROM sqlite_master WHERE name = ?")); - kind.bind({QString::fromLatin1(table)}); - if (!kind.next() || kind.value(0).toString() != QLatin1String("table")) { - setError(error, QStringLiteral("the project database has no table %1").arg(QLatin1String(table))); - return false; - } - } - - auto entry = [&document](const QVariant &name_attr, const QString &name, const QVariant &value, - const QVariant &attrs, const QString &default_tag, const QVariant &show = QVariant()) { - const QJsonObject o = QJsonDocument::fromJson(attrs.toString().toUtf8()).object(); - QDomElement e = document.createElement(o.value(QStringLiteral("tag")).toString(default_tag)); - if (!name_attr.isNull()) e.setAttribute(name, name_attr.toString()); - if (!show.isNull()) e.setAttribute(QStringLiteral("show"), show.toString()); - setAttributesJson(e, attrs); - if (!value.isNull()) e.appendChild(document.createTextNode(value.toString())); - return e; - }; //Project properties QHash meta; { - Statement s(db.m_db, QStringLiteral("SELECT key, value FROM meta")); + Statement s(db, QStringLiteral("SELECT key, value FROM meta")); while (s.next()) meta.insert(s.value(0).toString(), s.value(1)); } - //Format 2 (QElectroTech 0.200.1 development builds) keyed its rows - //apart from the items' uuids: "f1", "e1", "c1" where an item had - //none of its own, and every item's key in the folio file - const int format = meta.value(QStringLiteral("format")).toString().toInt(); - //A later format is read when the manifest's min-reader allows it - //(QetContainer::join()): its writer kept what this reads - if (format < 2) { - setError(error, QStringLiteral("the project database is of format %1, this reads format 2 on") - .arg(meta.value(QStringLiteral("format")).toString())); - return false; - } - const bool v2 = format == 2; - const QString folio_key = v2 ? QStringLiteral("id") : QStringLiteral("uuid"); - const QString folio_ref = v2 ? QStringLiteral("folio_id") : QStringLiteral("folio_uuid"); - const QString item_key = v2 ? QStringLiteral("key") : QStringLiteral("uuid"); - const QString element_ref = v2 ? QStringLiteral("element_key") : QStringLiteral("element_uuid"); - const QString show_column = v2 ? QStringLiteral("NULL") : QStringLiteral("show"); - const QStringList element_data = v2 ? ElementDataV2 : ElementData; - const QStringList conductor_data = v2 ? QStringList{QStringLiteral("uuid")} + ConductorData : ConductorData; - auto quoted = [](const QStringList &names) { - return QStringLiteral("\"") + names.join(QStringLiteral("\", \"")) + QStringLiteral("\""); - }; - //An item's key: the one it was given, else (format 3) its uuid - auto keyOf = [v2](QDomElement e, const QString &attribute) { - if (e.hasAttribute(attribute)) { - const QString key = e.attribute(attribute); - e.removeAttribute(attribute); - return key; - } - return v2 ? QString() : e.attribute(QStringLiteral("uuid")); - }; - //A moved block goes back where its marker is: the marker says it - //was there if (hasMarker(project, QStringLiteral("properties"))) { QDomElement props = document.createElement(QStringLiteral("properties")); setAttributesJson(props, meta.value(QStringLiteral("props_attrs"))); - Statement s(db.m_db, QStringLiteral("SELECT name, value, attrs, %1 FROM project_property ORDER BY ord") - .arg(show_column)); + Statement s(db, QStringLiteral("SELECT name, value, attrs, %1 FROM project_property ORDER BY ord") + .arg(db.showColumn())); while (s.next()) - props.appendChild(entry(s.value(0), QStringLiteral("name"), s.value(1), s.value(2), - QStringLiteral("property"), s.value(3))); + props.appendChild(entryElement(document, s.value(0), QStringLiteral("name"), s.value(1), s.value(2), + QStringLiteral("property"), s.value(3))); restoreWhitespace(props, meta.value(QStringLiteral("props_attrs"))); if (!replaceMarker(project, props)) { setError(error, QStringLiteral("the project's properties have no place")); @@ -764,7 +1112,7 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS if (meta.contains(QStringLiteral("terminal_strips"))) { StripRows rows; auto read = [&db](const QString &sql, int columns, QList *out) { - Statement s(db.m_db, sql); + Statement s(db, sql); while (s.next()) { QVariantList r; for (int i = 0 ; i < columns ; ++i) r << s.value(i); @@ -781,134 +1129,34 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS } } - //Every row must be an item's, once, on the folio the item is on: - //nothing enforces it in the file, which is untrusted - QSet folios_used, elements_used, conductors_used; - auto use = [error](QSet &used, const QString &key, const QString &what) { - if (used.contains(key)) { - setError(error, QStringLiteral("%1 %2 is in the folios twice").arg(what, key)); - return false; - } - used.insert(key); - return true; - }; + UsedRows used; for (QDomElement d : children(project, QStringLiteral("diagram"))) - { - const QString id = keyOf(d, FolioAttribute); - if (id.isEmpty()) continue; - if (!use(folios_used, id, QStringLiteral("folio"))) return false; - Statement f(db.m_db, QStringLiteral("SELECT %1, props_attrs FROM folio WHERE %2 = ?") - .arg(quoted(FolioData), folio_key)); - f.bind({id}); - if (!f.next()) { setError(error, QStringLiteral("folio %1 is not in the database").arg(id)); return false; } - QVariantList values; - for (int i = 0 ; i < FolioData.size() + 1 ; ++i) values << f.value(i); - put(d, FolioData, values, 0); - if (hasMarker(d, QStringLiteral("properties"))) { - QDomElement props = document.createElement(QStringLiteral("properties")); - setAttributesJson(props, values.at(FolioData.size())); - Statement p(db.m_db, QStringLiteral("SELECT name, value, attrs, %1 FROM folio_property WHERE %2 = ? ORDER BY ord") - .arg(show_column, folio_ref)); - p.bind({id}); - while (p.next()) - props.appendChild(entry(p.value(0), QStringLiteral("name"), p.value(1), p.value(2), - QStringLiteral("property"), p.value(3))); - restoreWhitespace(props, values.at(FolioData.size())); - if (!replaceMarker(d, props)) { - setError(error, QStringLiteral("folio %1's properties have no place").arg(id)); - return false; - } - } - - for (const QDomElement &list : children(d, QStringLiteral("elements"))) { - for (QDomElement e : children(list, QStringLiteral("element"))) { - const QString key = keyOf(e, KeyAttribute); - if (key.isEmpty()) continue; - if (!use(elements_used, key, QStringLiteral("symbol"))) return false; - Statement s(db.m_db, QStringLiteral("SELECT %1, info_attrs, links_attrs, %2 FROM element WHERE %3 = ?") - .arg(quoted(element_data), folio_ref, item_key)); - s.bind({key}); - if (!s.next()) { setError(error, QStringLiteral("symbol %1 is not in the database").arg(key)); return false; } - if (s.value(element_data.size() + 2).toString() != id) { - setError(error, QStringLiteral("symbol %1 is on folio %2, the database says another").arg(key, id)); - return false; - } - QVariantList v; - for (int i = 0 ; i < element_data.size() + 2 ; ++i) v << s.value(i); - put(e, element_data, v, 0); - const int n = element_data.size(); - QDomElement info, links; - if (hasMarker(e, QStringLiteral("elementInformations"))) { - info = document.createElement(QStringLiteral("elementInformations")); - setAttributesJson(info, v.at(n)); - Statement i(db.m_db, QStringLiteral("SELECT name, value, attrs, %1 FROM element_info WHERE %2 = ? ORDER BY ord") - .arg(show_column, element_ref)); - i.bind({key}); - while (i.next()) - info.appendChild(entry(i.value(0), QStringLiteral("name"), i.value(1), i.value(2), - QStringLiteral("elementInformation"), i.value(3))); - restoreWhitespace(info, v.at(n)); - } - if (hasMarker(e, QStringLiteral("links_uuids"))) { - links = document.createElement(QStringLiteral("links_uuids")); - setAttributesJson(links, v.at(n + 1)); - Statement l(db.m_db, QStringLiteral("SELECT linked_uuid, attrs FROM link WHERE %1 = ? ORDER BY ord").arg(element_ref)); - l.bind({key}); - while (l.next()) - links.appendChild(entry(l.value(0), QStringLiteral("uuid"), QVariant(), l.value(1), - QStringLiteral("link_uuid"))); - restoreWhitespace(links, v.at(n + 1)); - } - if ((!info.isNull() && !replaceMarker(e, info)) - || (!links.isNull() && !replaceMarker(e, links))) { - setError(error, QStringLiteral("symbol %1's information has no place").arg(key)); - return false; - } - } - } - for (const QDomElement &list : children(d, QStringLiteral("conductors"))) { - for (QDomElement c : children(list, QStringLiteral("conductor"))) { - const QString key = keyOf(c, KeyAttribute); - if (key.isEmpty()) continue; - if (!use(conductors_used, key, QStringLiteral("wire"))) return false; - Statement s(db.m_db, QStringLiteral("SELECT %1, %2 FROM conductor WHERE %3 = ?") - .arg(quoted(conductor_data), folio_ref, item_key)); - s.bind({key}); - if (!s.next()) { setError(error, QStringLiteral("wire %1 is not in the database").arg(key)); return false; } - if (s.value(conductor_data.size()).toString() != id) { - setError(error, QStringLiteral("wire %1 is on folio %2, the database says another").arg(key, id)); - return false; - } - QVariantList v; - for (int i = 0 ; i < conductor_data.size() ; ++i) v << s.value(i); - put(c, conductor_data, v, 0); - } - } - } - - //No row left over: each table's rows, counted, are the items read, - //and every entry belongs to a row - auto count = [&db](const QString &sql) { - Statement s(db.m_db, sql); - return s.next() ? s.value(0).toInt() : -1; - }; - const struct { QString sql; int expected; QString what; } checks[] = { - {QStringLiteral("SELECT count(*) FROM folio"), int(folios_used.size()), QStringLiteral("folio rows")}, - {QStringLiteral("SELECT count(*) FROM element"), int(elements_used.size()), QStringLiteral("symbol rows")}, - {QStringLiteral("SELECT count(*) FROM conductor"), int(conductors_used.size()), QStringLiteral("wire rows")}, - {QStringLiteral("SELECT count(*) FROM folio_property WHERE %1 NOT IN (SELECT %2 FROM folio)") - .arg(folio_ref, folio_key), 0, QStringLiteral("folio property rows of no folio")}, - {QStringLiteral("SELECT count(*) FROM element_info WHERE %1 NOT IN (SELECT %2 FROM element)") - .arg(element_ref, item_key), 0, QStringLiteral("information rows of no symbol")}, - {QStringLiteral("SELECT count(*) FROM link WHERE %1 NOT IN (SELECT %2 FROM element)") - .arg(element_ref, item_key), 0, QStringLiteral("link rows of no symbol")}}; - for (const auto &check : checks) { - const int found = count(check.sql); - if (found != check.expected) { - setError(error, QStringLiteral("the project database does not match its folios: %1 %2, %3 expected") - .arg(found).arg(check.what).arg(check.expected)); + if (!restoreDiagram(db, d, used, error)) + return false; + return noRowLeft(db, used, error); +} + +/** + @brief QetContainerDb::currentFormat + @return true if @p database, a project.sqlite, is of the format this + writes: a folio written as it was read keeps its rows, copied as they + are (stage D4) +*/ +bool QetContainerDb::currentFormat(const QByteArray &database) +{ + Database db; + if (!openDatabase(db, database, nullptr) || db.m_format != QetContainer::Format) + return false; + //Its rows are copied as they are: the same columns as this writes + const QList> columns = { + {QStringLiteral("folio"), int(FolioData.size()) + 3}, + {QStringLiteral("element"), int(ElementData.size()) + 4}, + {QStringLiteral("conductor"), int(ConductorData.size()) + 2}, + {QStringLiteral("folio_property"), 6}, {QStringLiteral("element_info"), 6}}; + for (const auto &table : columns) { + Statement s(db, QStringLiteral("SELECT count(*) FROM pragma_table_info('%1')").arg(table.first)); + if (!s.next() || s.value(0).toInt() != table.second) return false; - } } return true; } diff --git a/sources/container/qetcontainerdb.h b/sources/container/qetcontainerdb.h index a60c293b3..4f4b31408 100644 --- a/sources/container/qetcontainerdb.h +++ b/sources/container/qetcontainerdb.h @@ -39,8 +39,11 @@ */ namespace QetContainerDb { - QByteArray extract(QDomElement project, QString *error = nullptr); + QByteArray extract(QDomElement project, QString *error = nullptr, + const QByteArray &kept_from = QByteArray()); bool restore(QDomElement project, const QByteArray &database, QString *error = nullptr); + /// true if @p database is of the format this writes + bool currentFormat(const QByteArray &database); } #endif // QETCONTAINERDB_H diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index 68a98486e..b688865b9 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -49,6 +49,8 @@ #include #include #include +#include +#include #include #include #include @@ -137,7 +139,7 @@ void projectDataBase::updateDB() @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 + yet (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. @@ -357,6 +359,8 @@ struct DocumentElement QString type; QString sub_type; QVariant group; + QString location; + autonum::sequentialNumbers sequence; DiagramContext informations; QString label; QHash terminals; @@ -377,6 +381,27 @@ bool readSequence(const QDomElement &item, autonum::sequentialNumbers *sequence) return true; } + //The sequential values an element was saved with, as Element::fromXml() + //reads them, the older attributes included +autonum::sequentialNumbers elementSequence(const QDomElement &item) +{ + autonum::sequentialNumbers sequence; + if (readSequence(item, &sequence)) { + return sequence; + } + auto load = [&item](const QString &prefix, QStringList *list) { + for (int i = 1 ; !item.attribute(prefix + QString::number(i)).isEmpty() ; ++i) + list->append(item.attribute(prefix + QString::number(i))); + }; + load(QStringLiteral("sequ_"), &sequence.unit); + load(QStringLiteral("sequf_"), &sequence.unit_folio); + load(QStringLiteral("seqt_"), &sequence.ten); + load(QStringLiteral("seqtf_"), &sequence.ten_folio); + load(QStringLiteral("seqh_"), &sequence.hundred); + load(QStringLiteral("seqhf_"), &sequence.hundred_folio); + return sequence; +} + struct DocumentConductor { QString uuid; @@ -428,7 +453,7 @@ 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")); - //Built or not (QET_LAZY_FOLIOS): only their uuids are read, and the + //Built or not (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()) { @@ -607,6 +632,8 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString element.type = known->type; element.sub_type = known->sub_type; element.terminals = known->terminals; + element.location = element_xml.attribute(QStringLiteral("type")); + element.sequence = elementSequence(element_xml); const QUuid group = ItemGroups::read(element_xml); element.group = group.isNull() ? QVariant() : QVariant(group.toString()); element.informations.fromXml( @@ -808,6 +835,8 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString m_insert_elements_query.bindValue(QStringLiteral(":type"), element.type); m_insert_elements_query.bindValue(QStringLiteral(":sub_type"), element.sub_type); m_insert_elements_query.bindValue(QStringLiteral(":group_uuid"), element.group); + m_insert_elements_query.bindValue(QStringLiteral(":definition"), element.location); + m_insert_elements_query.bindValue(QStringLiteral(":sequence"), sequenceValue(element.sequence)); if (!m_insert_elements_query.exec()) { qDebug() << "projectDataBase::populateFromDocument element insert error : " << m_insert_elements_query.lastError(); } @@ -881,7 +910,7 @@ QSet projectDataBase::foliosWithUnjoinedWires() const @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 + folios not built yet (lazy folios) its rows came from the document and nothing can have changed them. A symbol whose definition was not found has no row. */ @@ -902,11 +931,466 @@ QHash projectDataBase::elementTypesOnFolios(const QSet &f return types; } +/** + @brief projectDataBase::symbolsOnFolios + @return the symbols on the folios @p folios, folio by folio in that + order and on each in the order of its rows -- the order the folio + places them -- read from the element table as it is (see + elementTypesOnFolios()). A symbol whose definition was not found has + no row. +*/ +QList projectDataBase::symbolsOnFolios(const QList &folios) const +{ + QList symbols; + QSqlQuery query(m_data_base); + query.prepare(QStringLiteral("SELECT uuid, type, definition, sequence FROM element " + "WHERE diagram_uuid = :folio ORDER BY rowid")); + for (const QUuid &folio : folios) { + query.bindValue(QStringLiteral(":folio"), folio.toString()); + if (!query.exec()) { + continue; + } + while (query.next()) { + symbols.append({QUuid(query.value(0).toString()), folio, query.value(1).toString(), + query.value(2).toString(), sequenceFromValue(query.value(3))}); + } + } + return symbols; +} + +/** + @brief projectDataBase::wiresOnFolios + @return the wires on the folios @p folios, folio by folio in that order, + read from the conductor table as it is (see elementTypesOnFolios()) +*/ +QList projectDataBase::wiresOnFolios(const QList &folios) const +{ + QList wires; + QSqlQuery query(m_data_base); + query.prepare(QStringLiteral("SELECT c.text, e1.type, e2.type FROM conductor c " + "LEFT JOIN element e1 ON e1.uuid = c.terminal1_element_uuid " + "LEFT JOIN element e2 ON e2.uuid = c.terminal2_element_uuid " + "WHERE c.diagram_uuid = :folio ORDER BY c.rowid")); + for (const QUuid &folio : folios) { + query.bindValue(QStringLiteral(":folio"), folio.toString()); + if (!query.exec()) { + continue; + } + while (query.next()) { + wires.append({query.value(0).toString(), query.value(1).toString(), + query.value(2).toString()}); + } + } + return wires; +} + +/** + @brief projectDataBase::elementLabel + @return the label the symbol @p element shows, read from the element + info table as it is (see elementTypesOnFolios()) +*/ +QString projectDataBase::elementLabel(const QUuid &element) const +{ + QSqlQuery query(m_data_base); + query.prepare(QStringLiteral("SELECT label FROM element_info WHERE element_uuid = :uuid")); + query.bindValue(QStringLiteral(":uuid"), element.toString()); + return query.exec() && query.next() ? query.value(0).toString() : QString(); +} + +/** + @brief projectDataBase::terminalName + @return the name of the terminal @p terminal of the symbol @p element, + read from the terminal table as it is (see elementTypesOnFolios()): a + terminal a wire ends on has a row +*/ +QString projectDataBase::terminalName(const QUuid &terminal, const QUuid &element) const +{ + QSqlQuery query(m_data_base); + query.prepare(QStringLiteral("SELECT name FROM terminal WHERE uuid = :uuid AND element_uuid = :element")); + query.bindValue(QStringLiteral(":uuid"), terminal.toString()); + query.bindValue(QStringLiteral(":element"), element.toString()); + return query.exec() && query.next() ? query.value(0).toString() : QString(); +} + +/** + @brief projectDataBase::potentialWires + @return the wires at the potential of @p wire, @p wire included, read + from the conductor, element and link tables as they are (see + elementTypesOnFolios()), followed as Conductor::relatedPotentialConductors() + follows them over every folio: through a wire's ends, from a folio + report to the terminals of the first symbol it is linked to, and from a + terminal block's terminal to its others. Nothing when a terminal block + on the way isolates its terminals (their groups are in its definition, + not in the database) or @p wire has no row. +*/ +std::optional> projectDataBase::potentialWires(const QUuid &wire) const +{ + using Node = std::pair; //element uuid, terminal uuid + QHash> ends; + QHash wires_at; + QHash> nodes_of; + { + QSqlQuery query(m_data_base); + query.exec(QStringLiteral("SELECT uuid, terminal1_element_uuid, terminal1_uuid, " + "terminal2_element_uuid, terminal2_uuid FROM conductor")); + while (query.next()) { + const Node a{query.value(1).toString(), query.value(2).toString()}; + const Node b{query.value(3).toString(), query.value(4).toString()}; + const QString uuid = query.value(0).toString(); + ends.insert(uuid, {a, b}); + for (const Node &n : {a, b}) { + wires_at[n] << uuid; + if (!nodes_of.value(n.first).contains(n)) nodes_of[n.first] << n; + } + } + } + const auto start = ends.constFind(wire.toString()); + if (start == ends.constEnd()) { + return std::nullopt; + } + QHash kinds; + { + QSqlQuery query(m_data_base); + query.exec(QStringLiteral("SELECT uuid, type FROM element")); + while (query.next()) + kinds.insert(query.value(0).toString(), ElementData::typeFromString(query.value(1).toString())); + } + QSet wires; + QSet seen; + QList todo{start->first, start->second}; + while (!todo.isEmpty()) { + const Node node = todo.takeLast(); + if (seen.contains(node)) continue; + seen.insert(node); + for (const QString &w : wires_at.value(node)) { + if (wires.contains(QUuid(w))) continue; + wires.insert(QUuid(w)); + const auto e = ends.value(w); + todo << e.first << e.second; + } + const ElementData::Type kind = kinds.value(node.first, ElementData::Simple); + if (kind & ElementData::AllReport) { + const QList partners = linkedSymbols(QUuid(node.first)); + if (!partners.isEmpty()) + todo << nodes_of.value(partners.first().toString()); + } else if (kind & ElementData::Terminal) { + if (QET::infoFlagIsTrue(m_element_information.value(QUuid(node.first)) + .value(QStringLiteral("potential_isolating")).toString())) + return std::nullopt; + todo << nodes_of.value(node.first); + } + } + return wires; +} + +/** + @brief projectDataBase::linkedSymbols + @return the symbols @p element is linked to, read from the link table + as it is (see elementTypesOnFolios()) +*/ +QList projectDataBase::linkedSymbols(const QUuid &element) const +{ + QList linked; + QSqlQuery query(m_data_base); + query.prepare(QStringLiteral("SELECT linked_uuid FROM link WHERE element_uuid = :uuid ORDER BY rowid")); + query.bindValue(QStringLiteral(":uuid"), element.toString()); + if (query.exec()) { + while (query.next()) + linked.append(QUuid(query.value(0).toString())); + } + return linked; +} + +/** + @brief projectDataBase::sequenceValue + @return @p sequence as the element table's sequence column holds it: + each part the file would save (sequentialNumbers::toXml()), by its tag, + as JSON; null when the file would save none. sequenceFromValue() reads + it back as reading the file would (sequentialNumbers::fromXml()). +*/ +QVariant projectDataBase::sequenceValue(const autonum::sequentialNumbers &sequence) +{ + QDomDocument document; + const QDomElement xml = sequence.toXml(document); + if (!xml.hasChildNodes()) { + return QVariant(); + } + QJsonObject parts; + for (QDomElement part = xml.firstChildElement() ; !part.isNull() ; + part = part.nextSiblingElement()) + parts.insert(part.tagName(), part.text()); + return QString::fromUtf8(QJsonDocument(parts).toJson(QJsonDocument::Compact)); +} + +/** + @brief projectDataBase::sequenceFromValue + @return the sequential numbers held by @p value (sequenceValue()) +*/ +autonum::sequentialNumbers projectDataBase::sequenceFromValue(const QVariant &value) +{ + autonum::sequentialNumbers sequence; + if (value.isNull()) { + return sequence; + } + const QJsonObject parts = QJsonDocument::fromJson(value.toString().toUtf8()).object(); + QDomDocument document; + QDomElement xml = document.createElement(QStringLiteral("sequentialNumbers")); + for (auto it = parts.constBegin() ; it != parts.constEnd() ; ++it) { + QDomElement part = document.createElement(it.key()); + part.appendChild(document.createTextNode(it.value().toString())); + xml.appendChild(part); + } + sequence.fromXml(xml); + return sequence; +} + +/** + @brief projectDataBase::canEditUnbuilt + @return true if the information of the symbol @p element can be changed + through the store alone, its folio not built (lazy folios): it is + placed once, on a folio not built, and nothing else shows its + information -- no symbol built (alone or with its folio) holds it, no + partner is linked to it, and it has no label formula, which could read + what changes +*/ +bool projectDataBase::canEditUnbuilt(const QUuid &element) const +{ + if (!hasElementInformation(element) || placedElementCount(element) != 0 + || !linkedSymbols(element).isEmpty() + || !m_element_information.value(element).value(QStringLiteral("formula")).toString().isEmpty()) { + return false; + } + QSqlQuery query(m_data_base); + query.prepare(QStringLiteral("SELECT COUNT(*) FROM element WHERE uuid = :uuid")); + query.bindValue(QStringLiteral(":uuid"), element.toString()); + return query.exec() && query.next() && query.value(0).toInt() == 1; +} + +/** + @brief projectDataBase::editUnbuiltElementInformation + Change the information of the symbol @p element, on a folio not built + (canEditUnbuilt()), to @p information, as Element::setElementInformations() + would: the store and the symbol's element info row. Building the symbol + takes it (takeUnbuiltEdit()); saving builds its folio first + (foliosWithUnbuiltEdits()). +*/ +void projectDataBase::editUnbuiltElementInformation(const QUuid &element, DiagramContext information) +{ + if (information.contains(QStringLiteral("formula_id")) + && information.value(QStringLiteral("formula")).toString().isEmpty()) { + information.remove(QStringLiteral("formula_id")); + } + QUuid folio; + { + QSqlQuery query(m_data_base); + query.prepare(QStringLiteral("SELECT diagram_uuid FROM element WHERE uuid = :uuid")); + query.bindValue(QStringLiteral(":uuid"), element.toString()); + if (query.exec() && query.next()) folio = QUuid(query.value(0).toString()); + } + //Whether the file has a block of information for it: a symbol + //saves one when it has any (Element::toXml()) + const bool had_information = m_unbuilt_edits.contains(element) + ? m_unbuilt_edits.value(element).had_information + : !m_element_information.value(element).keys().isEmpty(); + m_unbuilt_edits.insert(element, {information, folio, had_information}); + if (!storeElementInformation(element, information)) { + return; + } + m_content_changed = true; + if (!m_update_blocked) { + bindElementInfoValues(m_update_element_query, element.toString(), information, + information.value(QStringLiteral("label")).toString()); + if (!m_update_element_query.exec()) { + qDebug() << "projectDataBase::editUnbuiltElementInformation update error : " << m_update_element_query.lastError(); + } + } + emit dataBaseUpdated(); +} + +/** + @brief projectDataBase::takeUnbuiltEdit + @return the information @p element was given while its folio was not + built (editUnbuiltElementInformation()), forgotten here: the symbol + being built takes it. Nothing if it was given none. +*/ +std::optional projectDataBase::takeUnbuiltEdit(const QUuid &element) +{ + const auto edit = m_unbuilt_edits.constFind(element); + if (edit == m_unbuilt_edits.constEnd()) { + return std::nullopt; + } + const DiagramContext information = edit->information; + m_unbuilt_edits.erase(edit); + return information; +} + +/** + @brief projectDataBase::foliosWithUnbuiltEdits + @return the folios holding a symbol whose information was changed while + they were not built (editUnbuiltElementInformation()) +*/ +QSet projectDataBase::foliosWithUnbuiltEdits() const +{ + QSet folios; + for (const auto &edit : m_unbuilt_edits) folios.insert(edit.folio); + for (const auto &edit : m_unbuilt_wire_edits) folios.insert(edit.folio); + return folios; +} + +/** + @brief projectDataBase::unbuiltWire + @return true if the wire @p wire is on a folio not built (lazy folios): + it has a row and its properties are stored, and no wire built holds its + uuid. @p folio is set to its folio. +*/ +bool projectDataBase::unbuiltWire(const QUuid &wire, QUuid *folio) const +{ + if (placedConductorCount(wire) != 0 || !hasConductorProperties(wire)) { + return false; + } + QSqlQuery query(m_data_base); + query.prepare(QStringLiteral("SELECT diagram_uuid FROM conductor WHERE uuid = :uuid")); + query.bindValue(QStringLiteral(":uuid"), wire.toString()); + if (!query.exec() || !query.next()) { + return false; + } + if (folio) *folio = QUuid(query.value(0).toString()); + return !query.next(); +} + +/** + @brief projectDataBase::editUnbuiltConductorProperties + Give the wire @p wire, on a folio not built (unbuiltWire()), the + properties @p properties as Conductor::setProperties() would: the store, + its rows and its conductor row's text. Building it takes them + (takeUnbuiltWireEdit()); a .qetz saves them in its database (QETProject:: + toXml()), anything else builds its folio to save. +*/ +void projectDataBase::editUnbuiltConductorProperties(const QUuid &wire, const ConductorProperties &properties) +{ + QUuid folio; + if (!unbuiltWire(wire, &folio)) { + return; + } + m_unbuilt_wire_edits.insert(wire, {properties, folio}); + if (!storeConductorProperties(wire, properties)) { + return; + } + m_content_changed = true; + if (!m_update_blocked) { + m_update_conductor_query.bindValue(QStringLiteral(":uuid"), wire.toString()); + m_update_conductor_query.bindValue(QStringLiteral(":text"), properties.text); + if (!m_update_conductor_query.exec()) { + qDebug() << "projectDataBase::editUnbuiltConductorProperties update error : " << m_update_conductor_query.lastError(); + } + } + emit dataBaseUpdated(); +} + +/** + @brief projectDataBase::takeUnbuiltWireEdit + @return the properties @p wire was given while its folio was not built, + forgotten here: the wire being built takes them +*/ +std::optional projectDataBase::takeUnbuiltWireEdit(const QUuid &wire) +{ + const auto edit = m_unbuilt_wire_edits.constFind(wire); + if (edit == m_unbuilt_wire_edits.constEnd()) { + return std::nullopt; + } + const ConductorProperties properties = edit->properties; + m_unbuilt_wire_edits.erase(edit); + return properties; +} + +/** + @brief projectDataBase::unbuiltWireEditsOnFolio + @return the properties given to the wires of the folio @p folio while it + was not built, by wire +*/ +QHash projectDataBase::unbuiltWireEditsOnFolio(const QUuid &folio) const +{ + QHash edits; + for (auto it = m_unbuilt_wire_edits.cbegin() ; it != m_unbuilt_wire_edits.cend() ; ++it) + if (it->folio == folio) edits.insert(it.key(), it->properties); + return edits; +} + +/** + @brief projectDataBase::setPlacedConductorProperties + Give the wire @p wire its properties @p properties: through the wire when + it is built, through the store when it is not (unbuiltWire()). What + undoing and redoing a change of wires not built does, the wires having + perhaps been built since. + @return false if neither can +*/ +bool projectDataBase::setPlacedConductorProperties(const QUuid &wire, const ConductorProperties &properties) +{ + const QList> placed = m_placed_conductors.values(wire); + if (placed.size() == 1 && placed.first()) { + placed.first()->setProperties(properties); + return true; + } + if (placed.isEmpty() && unbuiltWire(wire)) { + editUnbuiltConductorProperties(wire, properties); + return true; + } + return false; +} + +/** + @brief projectDataBase::unbuiltEditsOnFolio + @return the information changed through the store of the symbols on the + folio @p folio, not built (editUnbuiltElementInformation()), by symbol. + @p need_build is set when a .qetz cannot save one in place of the rows it + was read with: the file has no block of information for the symbol, or + the change empties it (a symbol with none saves no block) +*/ +QHash projectDataBase::unbuiltEditsOnFolio(const QUuid &folio, bool *need_build) const +{ + QHash edits; + if (need_build) *need_build = false; + for (auto it = m_unbuilt_edits.cbegin() ; it != m_unbuilt_edits.cend() ; ++it) { + if (it->folio != folio) continue; + edits.insert(it.key(), it->information); + if (need_build && (!it->had_information || it->information.keys().isEmpty())) + *need_build = true; + } + return edits; +} + +/** + @brief projectDataBase::setPlacedElementInformation + Give the symbol @p element its information @p information: through the + symbol when it is built, through the store when it is not + (canEditUnbuilt()). What undoing and redoing a change of a symbol not + built does, the symbol having perhaps been built since. + @return false if neither can +*/ +bool projectDataBase::setPlacedElementInformation(const QUuid &element, const DiagramContext &information) +{ + const QList> placed = m_placed_elements.values(element); + if (placed.size() == 1 && placed.first()) { + placed.first()->setElementInformations(information); + elementInfoChanged(placed.first().data()); + return true; + } + if (placed.isEmpty() && m_unbuilt_edits.contains(element)) { + editUnbuiltElementInformation(element, information); + return true; + } + if (placed.isEmpty() && canEditUnbuilt(element)) { + editUnbuiltElementInformation(element, information); + return true; + } + return false; +} + /** @brief projectDataBase::folioUuidsOfElements @return the uuids of the folios the symbols @p elements are on, read 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 + symbol on a folio not built yet (lazy folios) its row came from the document and nothing can have changed it. */ QSet projectDataBase::folioUuidsOfElements(const QSet &elements) const @@ -1044,10 +1528,11 @@ QSqlQuery projectDataBase::newQuery(const QString &query, QString *error) { //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. + //(lazy folios) a read of them builds those first, and so does a + //read of the view made of them. Every other table was filled from + //the document. static const QRegularExpression drawing_tables( - QStringLiteral("\\b(shape|independent_text|image)\\b"), + QStringLiteral("\\b(shape|independent_text|image|drawing_item_view)\\b"), QRegularExpression::CaseInsensitiveOption); if (m_project && m_project->unloadedFolioCount() && drawing_tables.match(query).hasMatch()) { m_project->loadFolios(); @@ -1114,8 +1599,10 @@ int projectDataBase::excludedConductorCount() const return 0; } + //A folio not built (lazy folios) has none: reading a wire + //drops it unless both its ends are found (Conductor::fromXml()) int count = 0; - for (auto *diagram : m_project->diagrams()) + for (auto *diagram : m_project->builtFolios()) { const auto conductor_list = diagram->conductors(); for (auto *conductor : conductor_list) @@ -1300,11 +1787,12 @@ void projectDataBase::flushElementPositions() /** @brief projectDataBase::updateFolioPositions Write every folio's position and folio number again, after a folio was - added, removed or moved. + added, removed or moved. A folio not built yet (lazy folios) has + its position and title block already: it is not built for this. */ void projectDataBase::updateFolioPositions() { - for (auto diagram : m_project->diagrams()) + for (auto diagram : m_project->folios()) { m_diagram_order_changed.bindValue(":pos", m_project->folioIndex(diagram)+1); m_diagram_order_changed.bindValue(":uuid", diagram->uuid().toString()); @@ -1433,6 +1921,13 @@ void projectDataBase::removeDiagram(Diagram *diagram) void projectDataBase::diagramInfoChanged(Diagram *diagram) { m_content_changed = true; + //Not while the project is being read: a folio's header read gives + //it a folio number counted from the folios read so far ("1/1" for + //the first of three); the rows are filled once every folio is + //read, from the document or from the index a .qetz was saved with + if (m_update_blocked) { + return; + } bindDiagramInfoValues(m_update_diagram_info_query, diagram); if (!m_update_diagram_info_query.exec()) { @@ -1964,7 +2459,7 @@ 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. A folio not built - //yet (QET_LAZY_FOLIOS) has none: its items write their rows as it + //yet (lazy folios) has none: its items write their rows as it //is built. for (auto diagram : m_project->folios()) { @@ -2017,6 +2512,8 @@ bool projectDataBase::createDataBase() "type VARCHAR(50)," "sub_type VARCHAR(50)," "group_uuid VARCHAR(50)," + "definition TEXT," + "sequence TEXT," "FOREIGN KEY (diagram_uuid) REFERENCES diagram (uuid)" ")"); if (!query_.exec(element_table)) { @@ -3266,7 +3763,7 @@ void projectDataBase::prefillFromDocument(const QDomDocument &document) @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): + @p keep_unplaced, when some folios are not built yet (lazy folios): the store then answers for their items until they are. */ void projectDataBase::endPrefill(bool keep_unplaced) @@ -3478,7 +3975,7 @@ void projectDataBase::flushLinks() qDebug() << "projectDataBase::flushLinks insert error : " << insert.lastError(); } } - //Partners on a folio not built (QET_LAZY_FOLIOS), which it will + //Partners on a folio not built (lazy folios), which it will //be linked to once that folio is for (const auto &waiting : element->waitingLinks()) { @@ -3680,7 +4177,8 @@ void projectDataBase::prepareQuery() m_diagram_info_order_changed.prepare("UPDATE diagram_info SET folio = :folio WHERE diagram_uuid = :uuid"); //INSERT ELEMENT - QString insert_element_query("INSERT INTO element (uuid, diagram_uuid, pos, type, sub_type, group_uuid) VALUES (:uuid, :diagram_uuid, :pos, :type, :sub_type, :group_uuid)"); + QString insert_element_query("INSERT INTO element (uuid, diagram_uuid, pos, type, sub_type, group_uuid, definition, sequence) " + "VALUES (:uuid, :diagram_uuid, :pos, :type, :sub_type, :group_uuid, :definition, :sequence)"); m_insert_elements_query = QSqlQuery(m_data_base); m_insert_elements_query.prepare(insert_element_query); @@ -3795,6 +4293,8 @@ void projectDataBase::bindElementValues(QSqlQuery &query, Element *element, Diag query.bindValue(QStringLiteral(":type"), element_data.typeToString()); query.bindValue(QStringLiteral(":sub_type"), element_data.masterTypeToString()); query.bindValue(QStringLiteral(":group_uuid"), groupValue(element)); + query.bindValue(QStringLiteral(":definition"), element->location().path()); + query.bindValue(QStringLiteral(":sequence"), sequenceValue(element->sequenceStruct())); } /** diff --git a/sources/dataBase/projectdatabase.h b/sources/dataBase/projectdatabase.h index be3322c7d..6b5fcc38c 100644 --- a/sources/dataBase/projectdatabase.h +++ b/sources/dataBase/projectdatabase.h @@ -29,9 +29,12 @@ #include #include +#include + #include "../diagramcontext.h" #include "../titleblockproperties.h" #include "../conductorproperties.h" +#include "../autoNum/assignvariables.h" class Element; class QETProject; @@ -157,7 +160,7 @@ class projectDataBase : public QObject //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: + //A folio of a project opened with 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); @@ -166,7 +169,37 @@ class projectDataBase : public QObject QString prefillReport() const; QSet foliosWithUnjoinedWires() const; QSet folioUuidsOfElements(const QSet &elements) const; + bool canEditUnbuilt(const QUuid &element) const; + void editUnbuiltElementInformation(const QUuid &element, DiagramContext information); + std::optional takeUnbuiltEdit(const QUuid &element); + QSet foliosWithUnbuiltEdits() const; + QHash unbuiltEditsOnFolio(const QUuid &folio, bool *need_build = nullptr) const; + bool setPlacedElementInformation(const QUuid &element, const DiagramContext &information); + bool unbuiltWire(const QUuid &wire, QUuid *folio = nullptr) const; + void editUnbuiltConductorProperties(const QUuid &wire, const ConductorProperties &properties); + std::optional takeUnbuiltWireEdit(const QUuid &wire); + QHash unbuiltWireEditsOnFolio(const QUuid &folio) const; + bool setPlacedConductorProperties(const QUuid &wire, const ConductorProperties &properties); QHash elementTypesOnFolios(const QSet &folios) const; + /// A symbol placed on a folio, as the element table has it + struct FolioSymbol { + QUuid uuid; + QUuid folio; + QString kind; ///< ElementData::typeToString() + QString location; ///< its definition, ElementsLocation::path() + autonum::sequentialNumbers sequence; + }; + QList symbolsOnFolios(const QList &folios) const; + QList linkedSymbols(const QUuid &element) const; + /// A wire placed on a folio: its text and the kinds of the + /// symbols at its ends (ElementData::typeToString()) + struct FolioWire { QString text; QString kind1; QString kind2; }; + QList wiresOnFolios(const QList &folios) const; + QString elementLabel(const QUuid &element) const; + QString terminalName(const QUuid &terminal, const QUuid &element) const; + std::optional> potentialWires(const QUuid &wire) const; + static QVariant sequenceValue(const autonum::sequentialNumbers &sequence); + static autonum::sequentialNumbers sequenceFromValue(const QVariant &value); void addElement (Element *element); void removeElement (Element *element); @@ -303,6 +336,14 @@ class projectDataBase : public QObject //briefly gives a copy its original's uuid, so one uuid can have //two symbols until the copy's is renewed. QMultiHash> m_placed_elements; + /// Information changed through the store for symbols on folios + /// not built (editUnbuiltElementInformation()), with the folio + /// each is on: building the symbol takes it (takeUnbuiltEdit()) + struct UnbuiltEdit { DiagramContext information; QUuid folio; bool had_information; }; + QHash m_unbuilt_edits; + /// The same for wires (editUnbuiltConductorProperties()) + struct UnbuiltWireEdit { ConductorProperties properties; QUuid folio; }; + QHash m_unbuilt_wire_edits; QHash m_folio_titleblocks; QHash> m_placed_folios; QHash m_conductor_properties; @@ -314,7 +355,7 @@ class projectDataBase : public QObject //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) + //folio not built yet are right (lazy folios) bool m_rows_from_document = false; void keepUnbuiltFolioRows(); void restoreKeptRows(const QString &table, Diagram *folio, const QString &where); diff --git a/sources/diagram.cpp b/sources/diagram.cpp index d28d8a543..1f9f62cd1 100644 --- a/sources/diagram.cpp +++ b/sources/diagram.cpp @@ -775,13 +775,13 @@ 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 + opened with 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()) { + if (isLoaded()) { return; } if (m_project) { @@ -791,6 +791,99 @@ void Diagram::ensureLoaded() } } +/** + @brief Diagram::buildSymbols + Build, of this folio not built yet (lazy folios), the symbols + @p uuids only -- partners a shown folio draws from -- and keep them for + when the folio is built (stage D5b). They belong to the + folio's scene; the folio stays not built. + @return the symbols built now +*/ +QList Diagram::buildSymbols(const QSet &uuids) +{ + QList built; + if (isLoaded() || uuids.isEmpty()) { + return built; + } + const QDomElement xml = deferredXml(); + if (xml.isNull()) { + return built; + } + projectDataBase *db = m_project ? m_project->dataBase() : nullptr; + if (db) db->setBuildingFolio(true); + for (QDomElement element_xml : + QET::findInDomElement(xml, QStringLiteral("elements"), QStringLiteral("element"))) + { + const QUuid uuid(element_xml.attribute(QStringLiteral("uuid"))); + if (uuid.isNull() || !uuids.contains(uuid) || m_partial_elements.contains(uuid) + || !Element::valideXml(element_xml)) { + continue; + } + if (Element *element = buildElement(element_xml, m_partial_table, true)) { + m_partial_elements.insert(uuid, element); + //A partner unlinked or linked again: the folio is not + //saved as it was read any more + connect(element, &Element::linkedElementChanged, this, [this]() { + if (!isLoaded()) m_partial_changed = true; + }); + built << element; + } + } + if (db) db->setBuildingFolio(false); + return built; +} + +namespace { + //The title block fields nothing on a folio is computed from: a formula + //reads the folio's label, plant, location, extra fields and grid + //(autonum::FormulaContext), none of these +const QStringList ShownOnlyFields = { + QStringLiteral("title"), QStringLiteral("author"), QStringLiteral("date"), + QStringLiteral("indexrev"), QStringLiteral("version"), QStringLiteral("filename"), + QStringLiteral("titleblocktemplate"), QStringLiteral("titleblocktemplateCollection")}; + +QString headerWithoutShownOnly(const QString &header) +{ + QDomDocument document; + document.setContent(header); + QDomElement root = document.documentElement(); + for (const QString &field : ShownOnlyFields) root.removeAttribute(field); + return document.toString(); +} +} + +/** + @brief Diagram::headerChangeShowsOnly + @return true if what changed in this folio's own data since it was read + (headerChanged()) is only title block fields nothing on the folio is + computed from (its title, author, date, revision, file name, template): + its items, as read, are what building it would write +*/ +bool Diagram::headerChangeShowsOnly() +{ + return headerWithoutShownOnly(headerXml()) == headerWithoutShownOnly(m_opened_header); +} + +/** + @brief Diagram::deferredXmlWithHeader + @return this folio, not built, as it saves: its own data as it is now + (writeHeader()) and its items as read (deferredXml()), in @p document -- + when its items are what building it would write (headerChangeShowsOnly()) +*/ +QDomElement Diagram::deferredXmlWithHeader(QDomDocument &document) +{ + QDomElement folio = document.createElement(QStringLiteral("diagram")); + writeHeader(document, folio); + QSet header_children; + for (QDomElement child = folio.firstChildElement() ; !child.isNull() ; child = child.nextSiblingElement()) + header_children.insert(child.tagName()); + const QDomElement items = deferredXml(); + for (QDomElement child = items.firstChildElement() ; !child.isNull() ; child = child.nextSiblingElement()) + if (!header_children.contains(child.tagName())) + folio.appendChild(document.importNode(child, true)); + return folio; +} + /** @brief Diagram::buildDeferred Build this folio's items from the XML kept for later, and nothing else. @@ -800,17 +893,21 @@ void Diagram::ensureLoaded() */ bool Diagram::buildDeferred() { - if (m_deferred.isNull()) { + if (isLoaded()) { return false; } QDomElement xml = m_deferred; m_deferred = QDomElement(); + m_kept_part.clear(); 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; + m_partial_elements.clear(); + m_partial_table.clear(); + m_partial_changed = false; if (db) db->setBuildingFolio(false); return true; } @@ -1136,7 +1233,7 @@ 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 + //A folio of a project opened with lazy folios is written from //its built items (QETProject::toXml() may write one not built as //it was read instead) ensureLoaded(); @@ -1249,7 +1346,7 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) { //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 + //built late (lazy folios), whose scene order differed std::stable_sort(list_conductors.begin(), list_conductors.end(), [](Conductor *a, Conductor *b) { const QString ka = conductorSortKey(a), kb = conductorSortKey(b); @@ -1489,6 +1586,73 @@ void Diagram::folioSequentialsToXml(QHash &table_adr_id, + bool consider_informations) +{ + // cree un element dont le type correspond a l'id type + QString type_id = element_xml.attribute(QStringLiteral("type")); + ElementsLocation element_location; + if (type_id.startsWith(QStringLiteral("embed://"))) { + element_location = ElementsLocation(type_id, m_project); + } + else { + element_location = ElementsLocation(type_id); + } + + int state = 0; + Element *nvel_elmt = + ElementFactory::Instance() -> createElement( + element_location, nullptr, &state); + if (state) + { + QString debug_message = + QString("Diagram::fromXml() : Le chargement de " + "la description de l'element %1 a " + "echoue avec le code d'erreur %2").arg( + element_location.path()).arg(state); + qDebug() << qPrintable(debug_message); + delete nvel_elmt; + return nullptr; + } + + addItem(nvel_elmt); + //Loading fail, remove item from the diagram + if (!nvel_elmt->fromXml(element_xml, table_adr_id)) + { + removeItem(nvel_elmt); + delete nvel_elmt; + qDebug() << QStringLiteral("Diagram::fromXml() : Le chargement des parametres d'un element a echoue"); + return nullptr; + } else { + //A symbol saved without a uuid got a random one from + //Element::fromXml(): a different identity on every load, + //written out on the next save. Derive it instead from what + //the symbol is and where it sits on its folio -- never from + //the folio's index, so inserting or moving a folio does not + //change it. Only for a folio being loaded: a paste renews + //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')))); + } + ItemGroups::setGroup(nvel_elmt, ItemGroups::read(element_xml)); + return nvel_elmt; + } +return nullptr; +} + /** @brief Diagram::fromXml Imports the described schema into an XML document. @@ -1864,7 +2028,7 @@ bool Diagram::fromXml(QDomElement &document, return(true); } - //A project opened with QET_LAZY_FOLIOS builds a folio's items when + //A project opened with 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. @@ -1910,65 +2074,23 @@ bool Diagram::fromXml(QDomElement &document, //Load all elements from the XML QList added_elements; QHash table_adr_id; + if (m_loading_deferred) { + table_adr_id = m_partial_table; //the terminals of symbols built already + } for (auto element_xml : QET::findInDomElement(root, QStringLiteral("elements"), QStringLiteral("element"))) { if (!Element::valideXml(element_xml)) continue; - - // cree un element dont le type correspond a l'id type - QString type_id = element_xml.attribute(QStringLiteral("type")); - ElementsLocation element_location; - if (type_id.startsWith(QStringLiteral("embed://"))) { - element_location = ElementsLocation(type_id, m_project); - } - else { - element_location = ElementsLocation(type_id); - } - - int state = 0; - Element *nvel_elmt = - ElementFactory::Instance() -> createElement( - element_location, nullptr, &state); - if (state) - { - QString debug_message = - QString("Diagram::fromXml() : Le chargement de " - "la description de l'element %1 a " - "echoue avec le code d'erreur %2").arg( - element_location.path()).arg(state); - qDebug() << qPrintable(debug_message); - delete nvel_elmt; - continue; - } - - addItem(nvel_elmt); - //Loading fail, remove item from the diagram - if (!nvel_elmt->fromXml(element_xml, table_adr_id)) - { - removeItem(nvel_elmt); - delete nvel_elmt; - qDebug() << QStringLiteral("Diagram::fromXml() : Le chargement des parametres d'un element a echoue"); - } else { - //A symbol saved without a uuid got a random one from - //Element::fromXml(): a different identity on every load, - //written out on the next save. Derive it instead from what - //the symbol is and where it sits on its folio -- never from - //the folio's index, so inserting or moving a folio does not - //change it. Only for a folio being loaded: a paste renews - //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')))); + //Built already as another folio's partner (buildSymbols()) + if (m_loading_deferred) { + Element *built = m_partial_elements.value(QUuid(element_xml.attribute(QStringLiteral("uuid")))); + if (built) { + added_elements << built; + continue; } - ItemGroups::setGroup(nvel_elmt, ItemGroups::read(element_xml)); - added_elements << nvel_elmt; } + if (Element *nvel_elmt = buildElement(element_xml, table_adr_id, consider_informations)) + added_elements << nvel_elmt; } //Texts, images and shapes written before they carried a uuid (or diff --git a/sources/diagram.h b/sources/diagram.h index 93b7f6bd8..cddb4d77e 100644 --- a/sources/diagram.h +++ b/sources/diagram.h @@ -143,6 +143,12 @@ class Diagram : public QGraphicsScene QUuid m_uuid = QUuid::createUuid(); /// The XML of this folio's items, while they are not built yet QDomElement m_deferred; + QString m_kept_part; + QHash m_partial_elements; + QHash m_partial_table; + bool m_partial_changed = false; + Element *buildElement(QDomElement element_xml, QHash &table_adr_id, + bool consider_informations); QString m_opened_header; QByteArray m_kept_text; bool m_loading_deferred = false; @@ -212,9 +218,20 @@ class Diagram : public QGraphicsScene bool = true, DiagramContent * = nullptr); /// false while this folio's items wait in its XML to be - /// built (QET_LAZY_FOLIOS) + /// built (lazy folios) bool isLoaded() const {return m_deferred.isNull();} QDomElement deferredXml() const {return m_deferred;} + /// The part @p file of the .qetz the project was opened from + /// holds this folio as it is while not built: saving may copy it + /// (QETProject::toXml(), stage D4) + void setKeptPart(const QString &file) {m_kept_part = file;} + QString keptPart() const {return m_kept_part;} + QList buildSymbols(const QSet &uuids); + /// Symbols built as partners (buildSymbols()) changed since: + /// the folio must be built to be saved + bool partialChanged() const {return m_partial_changed;} + /// The symbols built as partners (buildSymbols()), by uuid + QHash partialElements() const {return m_partial_elements;} QString headerXml(); /// The folio's text in the file it was read from, while it is /// not built (QETProject::toXml()) @@ -224,6 +241,8 @@ class Diagram : public QGraphicsScene /// headerChanged() (QETProject::toXml()) void rememberHeader() {m_opened_header = headerXml();} bool headerChanged() {return headerXml() != m_opened_header;} + bool headerChangeShowsOnly(); + QDomElement deferredXmlWithHeader(QDomDocument &document); void ensureLoaded(); bool buildDeferred(); //The last argument is only for a copy fragment: when it is diff --git a/sources/diagramcontent.cpp b/sources/diagramcontent.cpp index fc54e83c9..af6d66dbe 100644 --- a/sources/diagramcontent.cpp +++ b/sources/diagramcontent.cpp @@ -79,13 +79,7 @@ DiagramContent::DiagramContent(Diagram *diagram, bool selected) : m_other_conductors << c; } - if (m_potential_conductors.isEmpty()) { - m_potential_conductors << c; - } else { - if (!potentialIsManaged(c->relatedPotentialConductors(true).values())) { - m_potential_conductors << c; - } - } + m_potential_candidates << c; break; } case DiagramImageItem::Type: { m_images << qgraphicsitem_cast(item); break;} @@ -379,12 +373,14 @@ DiagramContent &DiagramContent::operator+=(const DiagramContent &other) if(!m_selected_items.contains(qgi)) m_selected_items << qgi; - for (Conductor *c : other.m_potential_conductors) + QList potentials = potentialConductors(); + for (Conductor *c : other.potentialConductors()) { QList c_list = c->relatedPotentialConductors(true).values(); c_list << c; if (!potentialIsManaged(c_list)) { - m_potential_conductors << c; + potentials << c; + m_potential_conductors = potentials; } } @@ -402,17 +398,40 @@ DiagramContent &DiagramContent::operator+=(const DiagramContent &other) */ bool DiagramContent::potentialIsManaged(QList conductors) { + const QList potentials = potentialConductors(); bool b = false; for (Conductor *c : conductors) { - if (m_potential_conductors.contains(c)) + if (potentials.contains(c)) b = true; } return b; } +/** + @brief DiagramContent::potentialConductors + @return one wire of each potential of the wires of this content, worked + out the first time it is asked (see m_potential_conductors) +*/ +QList DiagramContent::potentialConductors() const +{ + if (!m_potential_conductors) { + QList potentials; + for (Conductor *c : m_potential_candidates) { + bool managed = false; + if (!potentials.isEmpty()) + for (Conductor *related : c->relatedPotentialConductors(true)) + if (potentials.contains(related)) { managed = true; break; } + if (!managed) + potentials << c; + } + m_potential_conductors = potentials; + } + return *m_potential_conductors; +} + /** @brief DiagramContent::hasTextEditing @return true if handle a text currently in editing interaction diff --git a/sources/diagramcontent.h b/sources/diagramcontent.h index 92d7c1caf..a693cfdf1 100644 --- a/sources/diagramcontent.h +++ b/sources/diagramcontent.h @@ -19,6 +19,7 @@ #define DIAGRAM_CONTENT_H #include +#include #include #include "../qetgraphicsitem/terminalelement.h" @@ -77,7 +78,6 @@ class DiagramContent QList m_conductors_to_update; QList m_conductors_to_move; QList m_other_conductors; - QList m_potential_conductors; QSet m_element_texts; QSet m_texts_groups; QList m_selected_items; @@ -89,6 +89,7 @@ class DiagramContent QList selectedTexts() const; QList selectedTextsGroup() const; QList conductors(int = AnyConductor) const; + QList potentialConductors() const; bool hasDeletableItems() const; bool hasCopiableItems() const; QList items(int = All) const; @@ -103,6 +104,12 @@ class DiagramContent private: int removePinnedGroups(); + /// The wires potentialConductors() is made from, in order + QList m_potential_candidates; + /// One wire of each potential, worked out when first asked: + /// following a potential goes through folio reports to other + /// folios, building those not built yet (lazy folios) + mutable std::optional> m_potential_conductors; }; QDebug &operator<<(QDebug, DiagramContent &); #endif diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index 5a1bc6590..7ece3ee4a 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -1649,7 +1649,7 @@ void DiagramView::editDiagramProperties() @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. + (lazy folios); then fit the view to them. */ void DiagramView::buildFolio() { diff --git a/sources/elementprovider.cpp b/sources/elementprovider.cpp index a493fd7fe..3bb4c4165 100644 --- a/sources/elementprovider.cpp +++ b/sources/elementprovider.cpp @@ -55,7 +55,7 @@ ElementProvider::ElementProvider(const QList &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 + (lazy folios) holding such a symbol are built first, and only them (QETProject::buildFoliosHolding()). */ QList ElementProvider::folios(ElementData::Types kinds, bool free_only) const @@ -98,7 +98,7 @@ QVector > ElementProvider::freeElement(ElementData::Types filt 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 + //(lazy folios) is not linked yet, but is not free: it will be const QSet waiting = m_project ? m_project->symbolsOnUnbuiltFolios() : QSet(); @@ -199,7 +199,14 @@ QVector ElementProvider::table( } } - for (auto d : allFolios()) { + //Only the folios holding a table are built for it + QList folios = m_diagram_list; + if (m_project) { + m_project->buildFoliosHoldingTables(); + folios = m_project->builtFolios(); + folios.removeOne(m_excluded); + } + for (auto d : std::as_const(folios)) { for (auto item_ : d->items()) { if(item_->type() == QetGraphicsTableItem::Type) diff --git a/sources/elementprovider.h b/sources/elementprovider.h index 5f839c38c..592088d83 100644 --- a/sources/elementprovider.h +++ b/sources/elementprovider.h @@ -55,7 +55,7 @@ class ElementProvider 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 + //so that a project opened with lazy folios builds only //those a search can find something on QETProject *m_project = nullptr; Diagram *m_excluded = nullptr; diff --git a/sources/projectview.cpp b/sources/projectview.cpp index 47bdc0719..158cc2ae5 100644 --- a/sources/projectview.cpp +++ b/sources/projectview.cpp @@ -341,13 +341,15 @@ QString ProjectView::askUserForFilePath(bool assign) { // ask the user for a filepath in order to save the project const QString qet_filter = tr("Project QElectroTech (*.qet)", "filetypes allowed when saving a project file"); const QString qetz_filter = tr("Zipped project QElectroTech (*.qetz)", "filetypes allowed when saving a project file"); - QString selected_filter = QETProject::isContainerPath(m_project->filePath()) ? qetz_filter : qet_filter; + //A project is saved as a .qetz unless a .qet is chosen + //(stage D1) + QString selected_filter = qetz_filter; QString filepath = QFileDialog::getSaveFileName( this, tr("Save as", "dialog title"), m_project -> currentDir() + "/" + tr("untitled") + (selected_filter == qetz_filter ? ".qetz" : ".qet"), - qet_filter + ";;" + qetz_filter, + qetz_filter + ";;" + qet_filter, &selected_filter ); @@ -725,6 +727,32 @@ QETResult ProjectView::saveAs() return(doSave()); } +/** + @brief ProjectView::saveCopyForOlderVersions + Ask for a .qet path and write the project there, for a version of + QElectroTech that cannot open a .qetz. The project keeps its file. + @return a QETResult object reflecting the situation; a valid one if + the operation was cancelled +*/ +QETResult ProjectView::saveCopyForOlderVersions() +{ + if (!m_project) return(noProjectResult()); + + QString base = m_project->filePath().isEmpty() + ? m_project->currentDir() + "/" + tr("untitled") + : m_project->filePath(); + for (const QString &suffix : {QStringLiteral(".qetz"), QStringLiteral(".qet")}) + if (base.endsWith(suffix, Qt::CaseInsensitive)) base.chop(suffix.size()); + QString path = QFileDialog::getSaveFileName( + this, + tr("Save a copy for an older QElectroTech", "dialog title"), + base + ".qet", + tr("Project QElectroTech (*.qet)", "filetypes allowed when saving a project file")); + if (path.isEmpty()) return(QETResult()); + if (!path.endsWith(".qet", Qt::CaseInsensitive)) path += ".qet"; + return(m_project->writeCopyForOlderVersions(path)); +} + /** Save project content, then write the project file. May call saveAs if no filepath was provided before. @@ -741,6 +769,31 @@ QETResult ProjectView::doSave() return(saveAs()); } + //A project read from a .qet is saved as a .qetz beside it: say so, + //and ask before replacing a .qetz already there + if (m_project->savesBesideOpenedFile()) { + const QString beside = m_project->besideContainerPath(); + const QString message = tr("This project was opened from %1. It is saved as %2, " + "beside it, in the zipped project format; %1 stays as " + "it is.\n\nTo write a file for a version of " + "QElectroTech that cannot open %2, use File > Save a " + "copy for an older QElectroTech.") + .arg(QFileInfo(m_project->filePath()).fileName(), QFileInfo(beside).fileName()); + if (QFileInfo::exists(beside)) { + if (QET::QetMessageBox::question( + this, tr("Save as a zipped project", "message box title"), + message + QStringLiteral("\n\n") + + tr("%1 already exists. Replace it?").arg(QFileInfo(beside).fileName()), + QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel) + != QMessageBox::Yes) { + return(QETResult()); + } + } else { + QET::QetMessageBox::information( + this, tr("Save as a zipped project", "message box title"), message); + } + } + // write to file QETResult result = m_project -> write(); updateWindowTitle(); @@ -1204,7 +1257,7 @@ void ProjectView::rebuildDiagramsMap() */ void ProjectView::tabChanged(int tab_id) { - //A folio opened without its items (QET_LAZY_FOLIOS) is built when + //A folio opened without its items (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))) diff --git a/sources/projectview.h b/sources/projectview.h index dfbd03987..eec92dcc7 100644 --- a/sources/projectview.h +++ b/sources/projectview.h @@ -131,6 +131,7 @@ class ProjectView : public QWidget QETResult save(); QETResult saveAs(); QETResult doSave(); + QETResult saveCopyForOlderVersions(); int cleanProject(); void updateWindowTitle(); void updateTabTitle(DiagramView *); diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index 798f29e23..c3a02bbda 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -978,6 +978,8 @@ void QETDiagramEditor::setUpActions() QAction *open_file = m_file_actions_group.addAction(QET::Icons::DocumentOpen, tr("&Open")); m_save_file = m_file_actions_group.addAction(QET::Icons::DocumentSave, tr("&Save")); m_save_file_as = m_file_actions_group.addAction(QET::Icons::DocumentSaveAs, tr("Save as")); + m_save_copy_for_older = m_file_actions_group.addAction(QET::Icons::DocumentSaveAs, + tr("Save a copy for an older QElectroTech")); m_close_file = m_file_actions_group.addAction(QET::Icons::ProjectClose, tr("&Close")); ShortcutManager::instance().registerAction(new_file, "diagrameditor.new_file", tr("Diagram editors"), QKeySequence::New); @@ -991,6 +993,19 @@ void QETDiagramEditor::setUpActions() m_close_file ->setStatusTip( tr("Closes the current project", "status bar tip") ); m_save_file ->setStatusTip( tr("Saves the current project and all its sheets", "status bar tip") ); m_save_file_as ->setStatusTip( tr("Saves the current project with a different file name", "status bar tip") ); + m_save_copy_for_older->setStatusTip(tr("Writes a copy of the current project as a .qet file, which " + "versions of QElectroTech that cannot open a .qetz open", + "status bar tip")); + ShortcutManager::instance().registerAction(m_save_copy_for_older, "diagrameditor.save_copy_for_older", + tr("Diagram editors"), QKeySequence()); + connect(m_save_copy_for_older, &QAction::triggered, this, [this]() { + if (ProjectView *project_view = currentProjectView()) { + const QETResult result = project_view->saveCopyForOlderVersions(); + if (!result.isOk()) { + showError(result); + } + } + }); connect(m_save_file_as, &QAction::triggered, this, &QETDiagramEditor::saveAs); connect(m_save_file, &QAction::triggered, this, &QETDiagramEditor::save); @@ -2760,6 +2775,7 @@ void QETDiagramEditor::slot_updateActions() m_close_file-> setEnabled(opened_project); m_save_file-> setEnabled(opened_project); m_save_file_as-> setEnabled(opened_project); + m_save_copy_for_older-> setEnabled(opened_project); m_rotate_texts-> setEnabled(editable_project); m_export_to_images-> setEnabled(opened_diagram); m_print-> setEnabled(opened_diagram); diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h index 2ff471b3f..f9287cf07 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -301,6 +301,7 @@ class QETDiagramEditor : public QETMainWindow *m_close_file, ///< Close current project file *m_save_file, ///< Save current project *m_save_file_as, ///< Save current project as a specific file + *m_save_copy_for_older, ///< Write a .qet copy for an older QElectroTech *m_find = nullptr, *m_jump_to_element = nullptr; ///< Open the "jump to element" quick-open popup diff --git a/sources/qetgraphicsitem/ViewItem/projectdbmodel.cpp b/sources/qetgraphicsitem/ViewItem/projectdbmodel.cpp index cf9525cc2..eb76c5662 100644 --- a/sources/qetgraphicsitem/ViewItem/projectdbmodel.cpp +++ b/sources/qetgraphicsitem/ViewItem/projectdbmodel.cpp @@ -323,7 +323,7 @@ void ProjectDBModel::setIdentifier(const QString &identifier) { /** @brief ProjectDBModel::folioBuilt - The folio of a table made while it was being built (QET_LAZY_FOLIOS) + The folio of a table made while it was being built (lazy folios) is built: read the database, as opening does once it has filled it. Nothing for any other model. */ @@ -391,7 +391,7 @@ 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 + //it after: a table on a folio built late (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) { diff --git a/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp b/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp index 0b6922ebf..6fe42bfcd 100644 --- a/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp +++ b/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp @@ -566,7 +566,7 @@ void QetGraphicsTableItem::initLink() if (!m_pending_previous_table_uuid.isNull()) { //The built folios only: a previous table on a folio not built - //yet (QET_LAZY_FOLIOS) is linked when that folio is + //yet (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); @@ -583,7 +583,7 @@ void QetGraphicsTableItem::initLink() /** @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 + still waiting for its folio to be built (lazy folios); null if none */ QUuid QetGraphicsTableItem::previousTableUuid() const { diff --git a/sources/qetgraphicsitem/conductor.cpp b/sources/qetgraphicsitem/conductor.cpp index a38071717..278fbbc6c 100644 --- a/sources/qetgraphicsitem/conductor.cpp +++ b/sources/qetgraphicsitem/conductor.cpp @@ -1154,6 +1154,11 @@ bool Conductor::fromXml(QDomElement &dom_element) m_freeze_label = dom_element.attribute("freezeLabel") == "true" ? true : false; + //Changed through the project database while its folio was not + //built (lazy folios): the change, not what the file says + if (diagram() && diagram()->project()) + if (const auto edited = diagram()->project()->dataBase()->takeUnbuiltWireEdit(m_uuid)) + pr = *edited; setProperties(pr); return retval; diff --git a/sources/qetgraphicsitem/element.cpp b/sources/qetgraphicsitem/element.cpp index 6a59bbcbc..03e600436 100644 --- a/sources/qetgraphicsitem/element.cpp +++ b/sources/qetgraphicsitem/element.cpp @@ -1115,6 +1115,11 @@ bool Element::fromXml(QDomElement &e, DiagramContext dc; dc.fromXml(e.firstChildElement(QStringLiteral("elementInformations")), QStringLiteral("elementInformation")); + //Changed through the project database while its folio was not + //built (lazy folios): the change, not what the file says + if (diagram() && diagram()->project()) + if (const auto edited = diagram()->project()->dataBase()->takeUnbuiltEdit(m_uuid)) + dc = *edited; //Load override properties (For now, only used when the element is a terminal) if (m_data.m_type == ElementData::Terminal) @@ -1250,17 +1255,41 @@ QDomElement Element::toXml( //if this element is linked to other elements, //save the uuid of each other elements - if (! isFree()) { + //A partner on a folio not built yet (lazy folios), which this + //symbol will be linked to, is saved too, in the order the symbol was + //saved with: saving needs no partner built (stage D5a) + QList> links; + if (keepsSavedLinks() && diagram() && diagram()->project()) { + const QSet waiting = diagram()->project()->waitingLinkTargets(); + QSet written; + for (const auto &link_info : std::as_const(tmp_uuids_link)) { + Element *linked = nullptr; + for (Element *elmt : std::as_const(connected_elements)) + if (elmt->uuid() == link_info.uuid && !written.contains(elmt)) { linked = elmt; break; } + if (linked) { + written.insert(linked); + links.append({linked->uuid(), m_group_index_map.value(linked, -1)}); + } else if (waiting.contains(link_info.uuid)) { + links.append({link_info.uuid, link_info.group_index}); + } + } + for (Element *elmt : std::as_const(connected_elements)) + if (!written.contains(elmt)) + links.append({elmt->uuid(), m_group_index_map.value(elmt, -1)}); + } else { + for (Element *elmt : std::as_const(connected_elements)) + links.append({elmt->uuid(), m_group_index_map.value(elmt, -1)}); + } + if (!links.isEmpty()) { QDomElement links_uuids = document.createElement(QStringLiteral("links_uuids")); - foreach (Element *elmt, connected_elements) { + for (const auto &link : std::as_const(links)) { QDomElement link_uuid = document.createElement(QStringLiteral("link_uuid")); - link_uuid.setAttribute(QStringLiteral("uuid"), elmt->uuid().toString()); + link_uuid.setAttribute(QStringLiteral("uuid"), link.first.toString()); // Save group index if assigned (for slave->master links) - int gi = m_group_index_map.value(elmt, -1); - if (gi >= 0) { - link_uuid.setAttribute(QStringLiteral("group_index"), gi); + if (link.second >= 0) { + link_uuid.setAttribute(QStringLiteral("group_index"), link.second); } links_uuids.appendChild(link_uuid); @@ -1630,10 +1659,11 @@ void Element::initLink(QETProject *prj) // if nothing to link return now if (tmp_uuids_link.isEmpty()) return; - //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()); + //The symbols built: on the folios built, and those built alone as + //partners on folios not built (Diagram::buildSymbols()). A partner + //not built yet (lazy folios) is linked when it is, and the + //saved list is kept until then, for the order it gives + ElementProvider ep(prj->folios()); QList uuids; for (const auto &linkInfo : tmp_uuids_link) { uuids.append(linkInfo.uuid); @@ -1659,7 +1689,7 @@ void Element::initLink(QETProject *prj) /** @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), + is not linked to yet: those on a folio not built yet (lazy folios), until initLink() runs again once it is */ QSet Element::pendingLinks() const diff --git a/sources/qetgraphicsitem/terminal.cpp b/sources/qetgraphicsitem/terminal.cpp index 51f081030..ed29d86d9 100644 --- a/sources/qetgraphicsitem/terminal.cpp +++ b/sources/qetgraphicsitem/terminal.cpp @@ -1099,7 +1099,7 @@ 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) + //The report's partner on a folio not built yet (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 @@ -1111,6 +1111,14 @@ QList relatedPotentialTerminal ( project->buildFoliosHoldingSymbols(waiting); } QList elmt_list = terminal -> parentElement() -> linkedElements(); + //A partner built alone (stage D5b) has no wires + //yet: its folio is built + if (!elmt_list.isEmpty() && project && elmt_list.first()->diagram() + && !elmt_list.first()->diagram()->isLoaded() + && project->state() != QETProject::ProjectParsingRunning) { + project->buildFolios({elmt_list.first()->diagram()}); + elmt_list = terminal->parentElement()->linkedElements(); + } if (!elmt_list.isEmpty()) { return (elmt_list.first()->terminals()); diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index a96d3d0de..caf4816af 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -18,6 +18,7 @@ #include "qetproject.h" #include "container/qetcontainer.h" +#include "container/qetcontainerdb.h" #include "ElementsCollection/xmlelementcollection.h" #include "autoNum/assignvariables.h" @@ -30,6 +31,8 @@ #include "autoNum/elementautonumschemecommand.h" #include "diagram.h" #include "qetgraphicsitem/element.h" +#include "qetgraphicsitem/conductor.h" +#include "qetgraphicsitem/dynamicelementtextitem.h" #include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h" #include "qetgraphicsitem/ViewItem/projectdbmodel.h" #include "qetapp.h" @@ -193,6 +196,8 @@ QETProject::QETProject(KAutoSaveFile *backup, QObject *parent) : } //Set the real path, instead of the path of the backup. setFilePath(backup->managedFile().path()); + //A project read from a .qet is saved as a .qetz beside it + m_opened_from_qet = !isContainerPath(m_file_path); delete backup; //Set the project to read only mode if the file it. @@ -337,7 +342,7 @@ 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): + //What changes the saved items of folios not built (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()) @@ -466,8 +471,12 @@ 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; + //Each folio's text is kept while it is not built (readDiagramsXml()): + //from a .qet only, a .qetz is a zip of parts + if (!content.startsWith("PK\x03\x04")) + m_opened_content = content; + //A project read from a .qet is saved as a .qetz beside it + m_opened_from_qet = !content.startsWith("PK\x03\x04"); QDomDocument xml_project; // PreserveSpacingOnlyNodes: without it, a text node that is entirely // whitespace -- e.g. a title-block custom variable deliberately set to @@ -485,12 +494,28 @@ QETProject::ProjectState QETProject::openFile(QFile *file) // QetContainer::parse() does exactly that, for a .qet and for each part // of a .qetz alike. QString read_error; + //A .qetz: its entries are kept, for a save to copy the parts of the + //folios not built (stage D4) + m_container_files.clear(); + m_folio_parts.clear(); + bool read = false; int container_format = 0; - if (!QetContainer::readDocument(content, &xml_project, &read_error, &m_container_pictures, - &container_format)) + if (QetContainer::isZip(content)) { + QList entries; + read = QetZip::fromBytes(content, &entries, &read_error) + && QetContainer::join(entries, &xml_project, &read_error, &m_folio_parts, + &m_container_pictures, &container_format); + if (read) + for (const QetZip::Entry &entry : std::as_const(entries)) + m_container_files.insert(entry.name, entry.data); + } else { + read = QetContainer::readDocument(content, &xml_project, &read_error); + } + if (!read) { qWarning().noquote() << "Unable to read" << fi.fileName() << ":" << read_error; m_container_pictures.clear(); + m_folio_parts.clear(); if(opened_here) { file->close(); } @@ -591,7 +616,7 @@ QList QETProject::diagrams() const /** @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() + or not (a project opened with lazy folios); see Diagram::isLoaded() */ QList QETProject::folios() const { @@ -603,9 +628,9 @@ QList QETProject::folios() const @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 + built yet holding a wire that still refers to a cable -- the only + wires the pass changes then, taking the reference away, read from the + folios' XML. */ QList QETProject::cableLabelFolios() { @@ -629,7 +654,7 @@ QList QETProject::cableLabelFolios() /** @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 + but for a project opened with lazy folios */ QList QETProject::builtFolios() const { @@ -722,6 +747,21 @@ void QETProject::buildFolios(const QList &folios) const for (Element *elmt : d->elements()) if (elmt->keepsSavedLinks()) elmt->initLink(self); + //A linked folio report's texts show a wire's properties: the + //report's own wire, else its partner's. A report built alone + //first, as a partner (buildPartnerSymbols()), linked before it had + //wires, and a report elsewhere may watch that partner: both look + //again now, as opening has them do once every wire exists (an + //unlinked report keeps its texts, as opening leaves them) + for (Diagram *d : std::as_const(built)) + for (Element *elmt : d->elements()) + if ((elmt->linkType() & Element::AllReport) && !elmt->linkedElements().isEmpty()) + { + emit elmt->linkedElementChanged(); + for (Element *partner : elmt->linkedElements()) + if (!built.contains(partner->diagram())) + emit partner->linkedElementChanged(); + } //Likewise a table whose previous table was on a folio not built for (Diagram *d : builtFolios()) for (QGraphicsItem *item : d->items()) @@ -746,7 +786,7 @@ void QETProject::buildFolios(const QList &folios) const /** @brief QETProject::buildFoliosHolding - Build the folios not built yet (QET_LAZY_FOLIOS) holding a symbol of + Build the folios not built yet (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 @@ -773,29 +813,22 @@ void QETProject::buildFoliosHolding(ElementData::Types kinds, bool free_only) for (auto it = types.cbegin() ; it != types.cend() ; ++it) placed.insert(it.key()); } + //The symbols and their links as the database has them 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"))) + for (const projectDataBase::FolioSymbol &symbol : m_data_base.symbolsOnFolios({d->uuid()})) { - const auto type = types.constFind(QUuid(element_xml.attribute(QStringLiteral("uuid")))); - if (type == types.constEnd() - || !(kinds & ElementData::typeFromString(*type))) { + if (!(kinds & ElementData::typeFromString(symbol.kind))) { 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"))))) { + for (const QUuid &link : m_data_base.linkedSymbols(symbol.uuid)) { + if (placed.contains(link)) { linked = true; break; } @@ -820,8 +853,9 @@ void QETProject::buildFoliosHolding(ElementData::Types kinds, bool free_only) /** @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 + Build the folios not built yet (lazy folios) holding one of the + symbols @p symbols, found in the project database once it holds the + folios not built, else 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) */ @@ -831,6 +865,15 @@ void QETProject::buildFoliosHoldingSymbols(const QSet &symbols) return; } QList to_build; + //Filled from the document, the database knows where every symbol is + if (m_data_base.rowsFromDocument()) { + const QSet folios = m_data_base.folioUuidsOfElements(symbols); + for (Diagram *d : std::as_const(m_diagrams_list)) + if (!d->isLoaded() && folios.contains(d->uuid())) + to_build << d; + buildFolios(to_build); + return; + } for (Diagram *d : std::as_const(m_diagrams_list)) { if (d->isLoaded()) { @@ -859,7 +902,7 @@ void QETProject::buildFoliosHoldingSymbols(const QSet &symbols) /** @brief QETProject::symbolsOnUnbuiltFolios @return the uuids of the symbols on the folios not built yet - (QET_LAZY_FOLIOS) that building them places + (lazy folios) that building them places */ QSet QETProject::symbolsOnUnbuiltFolios() const { @@ -967,11 +1010,43 @@ void QETProject::buildFolioToShow(Diagram *folio) 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; + qWarning().noquote() << "lazy folios: the partners of folio" << folio->folioIndex() + 1 + << "built alone, on" << to_build.size() << "folios"; } - buildFolios(to_build); + //The partners alone, not their folios (stage D5b) + buildPartnerSymbols(to_build, partners); +} + +/** + @brief QETProject::buildPartnerSymbols + Build on the folios @p folios, not built, the symbols @p partners alone + (Diagram::buildSymbols()), as opening builds them: linked to the symbols + built already, their texts refreshed, in the state opening has. +*/ +void QETProject::buildPartnerSymbols(const QList &folios, const QSet &partners) +{ + const ProjectState state = m_state; + m_state = ProjectParsingRunning; + QList built; + for (Diagram *d : folios) { + const QList symbols = d->buildSymbols(partners); + if (!symbols.isEmpty()) { + built += symbols; + linkElementsToElementAutoNums(d); + } + } + for (Element *element : std::as_const(built)) { + element->initLink(this); + for (auto &text : element->dynamicTextItems()) + text->refreshLabelConnection(); + } + //A built symbol linked to one of them puts its partners back in + //their saved order + for (Diagram *d : builtFolios()) + for (Element *element : d->elements()) + if (element->keepsSavedLinks()) + element->initLink(this); + m_state = state; } /** @@ -1046,6 +1121,8 @@ void QETProject::setFilePath(const QString &filepath) } m_next_backup_slot = 0; m_file_path = filepath; + //A path chosen is saved to as it is + m_opened_from_qet = false; QFileInfo fi(m_file_path); if (fi.isWritable()) { @@ -1813,9 +1890,9 @@ std::optional QETProject::elementAutoNumIdToLink( @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 + (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. + its sequential numbers and its kind as the database has them. */ QList QETProject::symbolNumbering() const { @@ -1846,38 +1923,27 @@ QList QETProject::symbolNumbering() const 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(); + //The symbols of the folios not built, from the database. A symbol + //it has no row for is one building leaves out (its definition is + //missing) + QList unbuilt_in_order; for (Diagram *d : m_diagrams_list) + if (!d->isLoaded()) unbuilt_in_order << d->uuid(); + const QHash titles_by_formula = elementAutoNumTitlesByFormula(); + for (const projectDataBase::FolioSymbol &row : m_data_base.symbolsOnFolios(unbuilt_in_order)) { - 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; - } + const DiagramContext info = m_data_base.elementInformation(row.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 = row.sequence; + const ElementData::Type kind = ElementData::typeFromString(row.kind); + symbol.numbered = kind != ElementData::Slave + && !(kind & ElementData::AllReport); + list << symbol; } return list; } @@ -2276,9 +2342,13 @@ void QETProject::autoFolioNumberingSelectedFolios(int from, @brief QETProject::toXml @return un document XML representant le projet */ -QDomDocument QETProject::toXml(QHash *kept_folios) +QDomDocument QETProject::toXml(QHash *kept_folios, QStringList *kept_parts) { - //With @p kept_folios, a folio not built (QET_LAZY_FOLIOS) is + //With @p kept_parts, for a .qetz: a + //folio not built is written as , its + //part and its rows copied by QetContainer::split() + //(stage D4) + //With @p kept_folios, a folio not built (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 @@ -2291,31 +2361,58 @@ QDomDocument QETProject::toXml(QHash *kept_folios) //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; + const bool keep = unloadedFolioCount() && !m_kept_folios_stale + && ((kept_folios && m_kept_folios_trusted) || (kept_parts && m_kept_parts_trusted)); if (keep) { - for (Diagram *diagram : std::as_const(m_diagrams_list)) - if (diagram->headerChanged()) + //Partners built alone and linked or unlinked since: their folio + //is not what was read + //and symbols whose information was changed through the project + //database while their folio was not built: built together + //-- a .qetz writes those in its database instead, when it can + const QSet edited = m_data_base.foliosWithUnbuiltEdits(); + QList changed; + for (Diagram *diagram : std::as_const(m_diagrams_list)) { + if (diagram->isLoaded()) continue; + bool need_build = !kept_parts; + if (edited.contains(diagram->uuid()) && kept_parts) + m_data_base.unbuiltEditsOnFolio(diagram->uuid(), &need_build); + if (diagram->partialChanged() || (edited.contains(diagram->uuid()) && need_build)) + changed << diagram; + } + buildFolios(changed); + //-- unless it changed only in what nothing is computed from (its + //title, date, revision...): written from its items as read + //(Diagram::deferredXmlWithHeader()) + for (Diagram *diagram : std::as_const(m_diagrams_list)) { + if (!diagram->headerChanged()) continue; + bool edits_need_build = false; + if (!diagram->isLoaded()) + m_data_base.unbuiltEditsOnFolio(diagram->uuid(), &edits_need_build); + if (diagram->isLoaded() || !diagram->headerChangeShowsOnly() || edits_need_build) 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()) + if (!diagram->isLoaded() && unbuiltFolioHasTables(diagram)) 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 + //partners: a table saves the table before it, and in a .qet a + //report the position of its partner. Build the folios of every + //table chain, and for a .qet of every partner still waiting, + //until none is. A symbol saves the partners it waits for itself + //(Element::toXml()), and a .qetz keeps no partner's text (D3c): + //saving a .qetz builds no partner (stage D5a) 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()); + if (kept_folios) + for (Element *element : diagram->elements()) + waiting.unite(element->pendingLinks()); for (Diagram *other : tableChainFolios(diagram)) if (!other->isLoaded()) chained.insert(other); } @@ -2329,6 +2426,7 @@ QDomDocument QETProject::toXml(QHash *kept_folios) } else { loadFolios(); } + m_waiting_link_targets = unloadedFolioCount() ? symbolsOnUnbuiltFolios() : QSet(); //QET_CHECK_ELEMENT_INFO_STORE=1: say whether the stores of symbol //information, folio title blocks and wire properties agree with every placed symbol, folio and //wire, at each save. A test and debugging aid; it changes nothing. @@ -2406,6 +2504,65 @@ QDomDocument QETProject::toXml(QHash *kept_folios) << "[" << diagram << "]"; + if (keep && !diagram->isLoaded() && diagram->headerChanged()) { + QDomElement xml_diagram = diagram->deferredXmlWithHeader(xml_doc); + //Its symbols whose information changed while it was not built + const QHash edits = m_data_base.unbuiltEditsOnFolio(diagram->uuid()); + for (const QDomElement &list : QET::findInDomElement(xml_diagram, QStringLiteral("elements"))) + for (QDomElement element = list.firstChildElement(QStringLiteral("element")) ; !element.isNull() ; + element = element.nextSiblingElement(QStringLiteral("element"))) { + const auto edit = edits.constFind(QUuid(element.attribute(QStringLiteral("uuid")))); + if (edit == edits.constEnd()) continue; + QDomElement information = xml_doc.createElement(QStringLiteral("elementInformations")); + edit->toXml(information, QStringLiteral("elementInformation")); + const QDomElement old_information = element.firstChildElement(QStringLiteral("elementInformations")); + element.replaceChild(information, old_information); //it has one (unbuiltEditsOnFolio()) + } + //Its wires whose properties changed + const QHash wire_edits = m_data_base.unbuiltWireEditsOnFolio(diagram->uuid()); + if (!wire_edits.isEmpty()) + for (const QDomElement &list : QET::findInDomElement(xml_diagram, QStringLiteral("conductors"))) + for (QDomElement wire = list.firstChildElement(QStringLiteral("conductor")) ; !wire.isNull() ; + wire = wire.nextSiblingElement(QStringLiteral("conductor"))) { + const auto edit = wire_edits.constFind(QUuid(wire.attribute(QStringLiteral("uuid")))); + if (edit == wire_edits.constEnd()) continue; + for (const auto &attribute : edit->attributes()) + wire.setAttribute(attribute.first, attribute.second); + } + xml_diagram.setAttribute("order", order_num ++); + project_root.appendChild(xml_diagram); + continue; + } + if (keep && !diagram->isLoaded() && kept_parts) { + auto placeholder = xml_doc.createElement(QStringLiteral("diagram")); + placeholder.setAttribute(QStringLiteral("qetz-kept-part"), diagram->keptPart()); + placeholder.setAttribute(QStringLiteral("uuid"), diagram->uuid().toString()); + //Symbols whose information changed while the folio was + //not built: the rows QetContainer::split() writes for them + const QHash edits = m_data_base.unbuiltEditsOnFolio(diagram->uuid()); + for (auto it = edits.cbegin() ; it != edits.cend() ; ++it) { + auto symbol = xml_doc.createElement(QStringLiteral("qetz-edited-element")); + symbol.setAttribute(QStringLiteral("uuid"), it.key().toString()); + auto information = xml_doc.createElement(QStringLiteral("elementInformations")); + it.value().toXml(information, QStringLiteral("elementInformation")); + symbol.appendChild(information); + placeholder.appendChild(symbol); + } + //Its wires whose properties changed: what the .qetz database + //holds of them (only that can change, QetScriptApi) + const QHash wire_edits = m_data_base.unbuiltWireEditsOnFolio(diagram->uuid()); + for (auto it = wire_edits.cbegin() ; it != wire_edits.cend() ; ++it) { + auto wire = xml_doc.createElement(QStringLiteral("qetz-edited-conductor")); + wire.setAttribute(QStringLiteral("uuid"), it.key().toString()); + for (const auto &attribute : it.value().attributes()) + wire.setAttribute(attribute.first, attribute.second); + placeholder.appendChild(wire); + } + kept_parts->append(diagram->keptPart()); + project_root.appendChild(placeholder); + ++order_num; + continue; + } if (keep && !diagram->isLoaded()) { auto placeholder = xml_doc.createElement(QStringLiteral("diagram")); placeholder.setAttribute(QStringLiteral("qet_kept_folio"), order_num); @@ -2447,6 +2604,7 @@ QDomDocument QETProject::toXml(QHash *kept_folios) // Write the elements collection. project_root.appendChild(m_elements_collection->root().cloneNode(true)); + m_waiting_link_targets.clear(); return(xml_doc); } @@ -2502,21 +2660,72 @@ bool QETProject::isContainerPath(const QString &path) document, in parts, zipped (#1440). The file is replaced only once the whole archive is written. */ -bool QETProject::writeContainer(const QDomDocument &project, const QString &path, QString *error) +bool QETProject::writeContainer(const QDomDocument &project, const QString &path, QString *error, + const QHash &kept_from) { - const QList entries = QetContainer::split( + QList entries = QetContainer::split( project, QStringLiteral("QElectroTech %1").arg(QetVersion::currentVersion().toString()), - error); + error, kept_from); return !entries.isEmpty() && QetZip::write(path, entries, error); } +/** + @brief QETProject::writeContainerFile + Write this project to @p path as a .qetz +*/ +bool QETProject::writeContainerFile(const QString &path, QString *error) +{ + //The folios not built, written as they were read, when that is + //what building them would write (toXml()); every other folio built + QStringList kept; + const QDomDocument document = toXml(nullptr, &kept); + return writeContainer(document, path, error, + kept.isEmpty() ? QHash() : m_container_files); +} + +/** + @brief QETProject::besideContainerPath + @return the .qetz a project read from a .qet is saved to: the same + name, beside it +*/ +QString QETProject::besideContainerPath() const +{ + QString path = m_file_path; + if (path.endsWith(QStringLiteral(".qet"), Qt::CaseInsensitive)) { + path.chop(4); + } + return path + QStringLiteral(".qetz"); +} + +/** + @brief QETProject::writeCopyForOlderVersions + Write this project as a .qet at @p path, which a QElectroTech that + reads no .qetz opens. The project keeps its own file and state. +*/ +QETResult QETProject::writeCopyForOlderVersions(const QString &path) +{ + QHash kept_folios; + QDomDocument xml_project(toXml(&kept_folios)); + QString error_message; + if (!QET::writeXmlText(spliceKeptFolios(xml_project.toString(4), kept_folios), + path, &error_message)) + return(error_message); + return(QETResult()); +} + QETResult QETProject::write() { // this operation requires a filepath if (m_file_path.isEmpty()) return(QString("unable to save project to file: no filepath was specified")); + //A project read from a .qet is saved as a .qetz beside it; the + //.qet stays as it was (stage D1) + if (m_opened_from_qet) { + setFilePath(besideContainerPath()); + } + // If the project was opened read-only, only refuse when the target // really can't be written: an existing file that is not writable, or a // new file (e.g. "Save As" to another location) whose directory is not @@ -2534,13 +2743,15 @@ QETResult QETProject::write() 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)) + //A container is written from the whole document: every folio + //built (stage D4 writes it from the database) + if (!writeContainerFile(m_file_path, &error_message)) return(error_message); } else { + //Folios not built (lazy folios) written as read when that + //is what building them would write (toXml()) QHash kept_folios; QDomDocument xml_project(toXml(&kept_folios)); if (!QET::writeXmlText(spliceKeptFolios(xml_project.toString(4), kept_folios), @@ -2781,24 +2992,77 @@ bool QETProject::usesElement(const ElementsLocation &location) const return(unbuiltFolioElements().contains(location)); } +/** + @brief QETProject::folioXml + @return the built folio @p folio written as saving writes it: its + symbols keep the links to partners on folios not built yet + (lazy folios), which Diagram::toXml() alone leaves out +*/ +QDomDocument QETProject::folioXml(Diagram *folio) +{ + m_waiting_link_targets = unloadedFolioCount() ? symbolsOnUnbuiltFolios() : QSet(); + const QDomDocument xml = folio->toXml(); + m_waiting_link_targets.clear(); + return xml; +} + +/** + @brief QETProject::buildFoliosHoldingTables + Build the folios not built yet (lazy folios) that hold a drawn + table: what a list of the project's tables needs, and no more +*/ +void QETProject::buildFoliosHoldingTables() +{ + QList to_build; + for (Diagram *d : std::as_const(m_diagrams_list)) + if (!d->isLoaded() && unbuiltFolioHasTables(d)) + 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 their tables"; + } + buildFolios(to_build); +} + +/** + @brief QETProject::unbuiltFolioHasTables + @return true if @p folio, not built yet (lazy folios), has a drawn + table, read from its XML +*/ +bool QETProject::unbuiltFolioHasTables(Diagram *folio) const +{ + return !folio->deferredXml().elementsByTagName(QetGraphicsTableItem::xmlTagName()).isEmpty(); +} + /** @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()) + (lazy folios), each once, read from the project database once it + holds those folios, else 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"))); + if (m_data_base.rowsFromDocument()) { + QList unbuilt; + for (Diagram *diagram : m_diagrams_list) + if (!diagram->isLoaded()) unbuilt << diagram->uuid(); + for (const projectDataBase::FolioSymbol &symbol : m_data_base.symbolsOnFolios(unbuilt)) + types.insert(symbol.location); + } else { + 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; @@ -3056,11 +3320,11 @@ void QETProject::readProjectXml(QDomDocument &xml_project) //built m_data_base.prefillFromDocument(xml_project); - //QET_LAZY_FOLIOS leaves the folios to be built when needed only + //Folios are left to be built when needed (lazyFolios()) only //when the database can be filled from the document: otherwise //filling it from the folios builds every one of them anyway, late //and more slowly than reading them now - m_lazy_open = qEnvironmentVariableIntValue("QET_LAZY_FOLIOS") == 1; + m_lazy_open = lazyFolios(); if (m_lazy_open) { const QString why = projectDataBase::documentRefusal(xml_project); if (!why.isEmpty()) { @@ -3115,9 +3379,16 @@ void QETProject::readProjectXml(QDomDocument &xml_project) for (Diagram *diagram : std::as_const(m_diagrams_list)) if (!diagram->isLoaded() && diagram->keptText().isEmpty()) m_kept_folios_trusted = false; + m_kept_parts_trusted = !m_folio_parts.isEmpty() && unloadedFolioCount() > 0 + && m_project_qet_version == QetVersion::currentVersion() + && m_data_base.rowsFromDocument() + && QetContainerDb::currentFormat(m_container_files.value(QStringLiteral("project.sqlite"))); + for (Diagram *diagram : std::as_const(m_diagrams_list)) + if (!diagram->isLoaded() && diagram->keptPart().isEmpty()) + m_kept_parts_trusted = false; m_kept_folios_stale = false; m_edited_since_open = false; - if (m_kept_folios_trusted) + if (m_kept_folios_trusted || m_kept_parts_trusted) for (Diagram *diagram : std::as_const(m_diagrams_list)) diagram->rememberHeader(); if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS_TRACE") == 1) @@ -3140,6 +3411,15 @@ void QETProject::readProjectXml(QDomDocument &xml_project) m_state = Ok; } +/** + @brief QETProject::lazyFolios + @see the declaration +*/ +bool QETProject::lazyFolios() +{ + return qEnvironmentVariableIntValue("QET_EAGER_FOLIOS") != 1; +} + /** @brief QETProject::readDiagramsXml Load the diagrams from the xml description of the project. @@ -3236,6 +3516,8 @@ void QETProject::readDiagramsXml(QDomDocument &xml_project) m_deferring_folios = false; if (!diagram->isLoaded() && !line_starts.isEmpty()) diagram->setKeptText(folio_text(diagram_xml_element)); + if (!diagram->isLoaded() && m_folio_parts.size() == diagram_nodes.length()) + diagram->setKeptPart(m_folio_parts.at(i)); m_data_base.placeFolio(diagram); if(dlgWaiting) dlgWaiting->setDetail(diagram->title()); diff --git a/sources/qetproject.h b/sources/qetproject.h index e5d0c2220..621097e55 100644 --- a/sources/qetproject.h +++ b/sources/qetproject.h @@ -32,6 +32,7 @@ #include "wiringrules.h" #include "diagram.h" #include "autoNum/assignvariables.h" +#include "container/qetcontainerdb.h" #ifdef BUILD_WITHOUT_KF # include "ui/nokde/kautosavefile.h" #else @@ -45,6 +46,7 @@ #include #include +#include #include class Diagram; @@ -123,7 +125,7 @@ class QETProject : public QObject ProjectState state() const; QList diagrams() const; //The folios whether their items are built or not - //(QET_LAZY_FOLIOS); diagrams() builds them + //(lazy folios); diagrams() builds them QList folios() const; bool deferringFolios() const {return m_deferring_folios;} int unloadedFolioCount() const; @@ -131,9 +133,15 @@ class QETProject : public QObject QList cableLabelFolios(); QList foliosWithWiresNotReconnected(); void buildFolioToShow(Diagram *folio); + void buildPartnerSymbols(const QList &folios, const QSet &partners); void buildFoliosHolding(ElementData::Types kinds, bool free_only); void buildFoliosHoldingSymbols(const QSet &symbols); + void buildFoliosHoldingTables(); QSet symbolsOnUnbuiltFolios() const; + /// While saving: the symbols on folios not built a built symbol + /// may be saved linked to (Element::toXml()) + QSet waitingLinkTargets() const {return m_waiting_link_targets;} + QDomDocument folioXml(Diagram *folio); QSet tableChainFolios(Diagram *folio) const; void loadFolios() const; void buildFolios(const QList &folios) const; @@ -155,6 +163,12 @@ class QETProject : public QObject /// process can destroy the project before the write finishes (crash). static void setBackupEnabled(bool enabled); + /// Whether a folio is built only when it is needed, not all on + /// opening (stage D6): yes, unless the + /// environment variable QET_EAGER_FOLIOS=1 asks for every folio + /// built on opening, as QElectroTech did before + static bool lazyFolios(); + /// Number of crash-recovery snapshots kept per project, written in /// turn by writeBackup(), so one bad write cannot replace the only copy static constexpr int BackupGenerations = 3; @@ -260,7 +274,7 @@ class QETProject : public QObject 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 + /// not (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 @@ -314,7 +328,8 @@ class QETProject : public QObject void autoFolioNumberingNewFolios (); void autoFolioNumberingSelectedFolios(int, int, const QString&); - QDomDocument toXml(QHash *kept_folios = nullptr); + QDomDocument toXml(QHash *kept_folios = nullptr, + QStringList *kept_parts = nullptr); static QString spliceKeptFolios(QString text, const QHash &kept_folios); bool close(); QETResult write(); @@ -328,6 +343,7 @@ class QETProject : public QObject bool usesElement(const ElementsLocation &) const; QList unusedElements() const; QList unbuiltFolioElements() const; + bool unbuiltFolioHasTables(Diagram *folio) const; bool usesTitleBlockTemplate(const TitleBlockTemplateLocation &); bool projectWasModified(); bool projectOptionsWereModified(); @@ -439,8 +455,16 @@ class QETProject : public QObject ProjectState openFile(QFile *file); public: static bool isContainerPath(const QString &path); + /// Saving writes a .qetz: one read from a .qet is saved as + /// a .qetz beside it, the .qet left as it is + /// (stage D1) + bool savesBesideOpenedFile() const {return m_opened_from_qet;} + QString besideContainerPath() const; + QETResult writeCopyForOlderVersions(const QString &path); static bool writeContainer(const QDomDocument &project, const QString &path, - QString *error = nullptr); + QString *error = nullptr, + const QHash &kept_from = QHash()); + bool writeContainerFile(const QString &path, QString *error = nullptr); private: static QUuid derivedUuid(const QByteArray &content); void refresh(); @@ -527,6 +551,16 @@ class QETProject : public QObject /// 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; + /// Read from a .qet whose path nobody chose since: saved beside it + bool m_opened_from_qet = false; + /// Saving a .qetz may copy the part of a folio not built + /// (toXml(), stage D4) + bool m_kept_parts_trusted = false; + QSet m_waiting_link_targets; + /// The entries of the .qetz opened, and the part each folio + /// was read from, in order + QHash m_container_files; + QStringList m_folio_parts; bool m_kept_folios_stale = false; /// Something was edited since the project was opened: a table /// filled from the database may have changed diff --git a/sources/scripting/liveserver.cpp b/sources/scripting/liveserver.cpp index 132b244f8..167f50930 100644 --- a/sources/scripting/liveserver.cpp +++ b/sources/scripting/liveserver.cpp @@ -919,11 +919,11 @@ QJsonObject LiveServer::snapshot(const QString &raw_path) if (raw_path.trimmed().isEmpty() || info.isRelative()) return failure(QStringLiteral("path must be absolute")); if (info.exists()) return failure(QStringLiteral("%1 already exists").arg(raw_path)); - QDomDocument xml(project->toXml()); QString error; if (QETProject::isContainerPath(info.absoluteFilePath()) - ? !QETProject::writeContainer(xml, info.absoluteFilePath(), &error) - : !QET::writeXmlFile(xml, info.absoluteFilePath(), &error)) + ? !project->writeContainerFile(info.absoluteFilePath(), &error) + : ![&]() { QDomDocument xml(project->toXml()); + return QET::writeXmlFile(xml, info.absoluteFilePath(), &error); }()) return failure(QStringLiteral("snapshot not written: %1").arg(error)); return {{QStringLiteral("ok"), true}, {QStringLiteral("path"), info.absoluteFilePath()}, {QStringLiteral("folio"), e->currentDiagramView() diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index 8561aa8d7..3ffe17358 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -126,7 +126,7 @@ QString QetScriptApi::filePath() const int QetScriptApi::folioCount() const { - return m_project ? m_project->diagrams().count() : 0; + return m_project ? m_project->folios().count() : 0; } /** @@ -183,7 +183,7 @@ QStringList QetScriptApi::signatures() int QetScriptApi::currentFolio() const { if (!m_project) return -1; - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (m_view && m_view->diagram()) { return diagrams.indexOf(m_view->diagram()); } @@ -193,7 +193,7 @@ int QetScriptApi::currentFolio() const QString QetScriptApi::folioTitle(int index) const { if (!m_project) return QString(); - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (index < 0 || index >= diagrams.count()) return QString(); return diagrams.at(index)->title(); } @@ -207,7 +207,7 @@ QString QetScriptApi::folioTitle(int index) const QString QetScriptApi::folioUuid(int index) const { if (!m_project) return QString(); - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (index < 0 || index >= diagrams.count()) return QString(); return diagrams.at(index)->uuid().toString(); } @@ -222,7 +222,7 @@ int QetScriptApi::folioIndex(const QString &uuid) const if (!m_project) return -1; const QUuid wanted(uuid); if (wanted.isNull()) return -1; - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); for (int i = 0 ; i < diagrams.count() ; ++i) { if (diagrams.at(i)->uuid() == wanted) return i; } @@ -232,18 +232,18 @@ int QetScriptApi::folioIndex(const QString &uuid) const int QetScriptApi::elementCount(int folioIndex) const { if (!m_project) return 0; - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return 0; - DiagramContent content(diagrams.at(folioIndex), false); + DiagramContent content(built(diagrams.at(folioIndex)), false); return content.m_elements.count(); } int QetScriptApi::conductorCount(int folioIndex) const { if (!m_project) return 0; - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return 0; - DiagramContent content(diagrams.at(folioIndex), false); + DiagramContent content(built(diagrams.at(folioIndex)), false); return content.conductors(DiagramContent::AnyConductor).count(); } @@ -434,11 +434,11 @@ bool QetScriptApi::save(const QString &output) return true; } - QDomDocument xml_doc(m_project->toXml()); QString error_message; if (QETProject::isContainerPath(output) - ? !QETProject::writeContainer(xml_doc, output, &error_message) - : !QET::writeXmlFile(xml_doc, output, &error_message)) { + ? !m_project->writeContainerFile(output, &error_message) + : ![&]() { QDomDocument xml_doc(m_project->toXml()); + return QET::writeXmlFile(xml_doc, output, &error_message); }()) { log(QStringLiteral("qet.save: %1").arg(error_message)); return false; } @@ -462,6 +462,41 @@ void QetScriptApi::setLive(QStringList *log) m_live_log = log; } +/** + @brief QetScriptApi::unbuiltInformation + @return the information of the symbol @p elementUuid on the folio + @p folioIndex when that folio is not built (lazy folios) and the + symbol's information can be read and changed through the project + database alone (projectDataBase::canEditUnbuilt()); nothing otherwise, + the folio to be built +*/ +std::optional QetScriptApi::unbuiltInformation(int folioIndex, const QString &elementUuid) const +{ + if (!m_project || folioIndex < 0 || folioIndex >= m_project->folios().count()) return std::nullopt; + Diagram *folio = m_project->folios().at(folioIndex); + const QUuid uuid(elementUuid); + projectDataBase *database = m_project->dataBase(); + if (folio->isLoaded() || !database->rowsFromDocument() + || !database->folioUuidsOfElements({uuid}).contains(folio->uuid()) + || !database->canEditUnbuilt(uuid)) { + return std::nullopt; + } + return database->elementInformation(uuid); +} + +/** + @brief QetScriptApi::built + @return @p folio, built if it was not yet (lazy folios), as showing + it builds it: what a call reading or changing its items needs. A + folio's own data (title block, border, wire defaults) needs no build. +*/ +Diagram *QetScriptApi::built(Diagram *folio) const +{ + if (folio && m_project) + m_project->buildFolioToShow(folio); + return folio; +} + /** @brief QetScriptApi::findElement @param folioIndex @@ -472,9 +507,9 @@ void QetScriptApi::setLive(QStringList *log) Element *QetScriptApi::findElement(int folioIndex, const QString &elementUuid) const { if (!m_project) return nullptr; - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return nullptr; - DiagramContent content(diagrams.at(folioIndex), false); + DiagramContent content(built(diagrams.at(folioIndex)), false); for (Element *elmt : std::as_const(content.m_elements)) { if (elmt->uuid().toString() == elementUuid) return elmt; @@ -653,6 +688,19 @@ bool QetScriptApi::setInfoKey(int folioIndex, const QString &elementUuid, log(QStringLiteral("qet.%1: \"%2\" is internal, follow a numbering with numberElement()") .arg(caller, key)); return false; + } + //A symbol on a folio not built whose information nothing else + //shows is changed through the project database, its folio left + //unbuilt (stage D5) + if (key != QETInformation::ELMT_FORMULA) { + if (const auto stored = unbuiltInformation(folioIndex, elementUuid)) { + if (stored->value(key).toString() == value) return true; // nothing to push + DiagramContext new_info = *stored; + new_info.addValue(key, value); + m_project->undoStack()->push(new ChangeStoredElementInformationCommand( + m_project, QUuid(elementUuid), *stored, new_info)); + return true; + } } Element *element = findElement(folioIndex, elementUuid); if (!element) return false; @@ -705,9 +753,9 @@ QString QetScriptApi::addElement(int folioIndex, const QString &locationPath, do log(QStringLiteral("qet.addElement: project is read-only")); return QString(); } - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString(); - Diagram *diagram = diagrams.at(folioIndex); + Diagram *diagram = built(diagrams.at(folioIndex)); // ElementsLocation::setPath() forces ANY path to embed:// as soon as a // non-null project is given, common://custom:// included -- passing @@ -817,7 +865,7 @@ bool QetScriptApi::deleteElement(int folioIndex, const QString &elementUuid) } Element *element = findElement(folioIndex, elementUuid); if (!element) return false; - Diagram *diagram = m_project->diagrams().at(folioIndex); + Diagram *diagram = built(m_project->folios().at(folioIndex)); DiagramContent content; content.m_elements << element; @@ -906,9 +954,9 @@ QStringList QetScriptApi::elementUuids(int folioIndex) const { QStringList uuids; if (!m_project) return uuids; - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return uuids; - DiagramContent content(diagrams.at(folioIndex), false); + DiagramContent content(built(diagrams.at(folioIndex)), false); for (Element *elmt : std::as_const(content.m_elements)) { uuids << elmt->uuid().toString(); } @@ -996,6 +1044,8 @@ int QetScriptApi::terminalIndex(int folioIndex, const QString &elementUuid, QString QetScriptApi::elementInfo(int folioIndex, const QString &elementUuid, const QString &key) const { + if (const auto stored = unbuiltInformation(folioIndex, elementUuid)) + return stored->value(key).toString(); Element *element = findElement(folioIndex, elementUuid); if (!element) return QString(); return element->elementInformations().value(key).toString(); @@ -1077,7 +1127,7 @@ bool QetScriptApi::addConductor(int folioIndex, return false; } - Diagram *diagram = m_project->diagrams().at(folioIndex); + Diagram *diagram = built(m_project->folios().at(folioIndex)); ConductorCreator creator(diagram, terminals); Q_UNUSED(creator) @@ -1110,10 +1160,10 @@ QStringList QetScriptApi::conductors(int folioIndex) const { QStringList list; if (!m_project) return list; - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return list; - DiagramContent content(diagrams.at(folioIndex), false); + DiagramContent content(built(diagrams.at(folioIndex)), false); const QList all = content.conductors(DiagramContent::AnyConductor); for (Conductor *c : all) { @@ -1132,10 +1182,10 @@ QStringList QetScriptApi::conductorUuids(int folioIndex) const { QStringList list; if (!m_project) return list; - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return list; - DiagramContent content(diagrams.at(folioIndex), false); + DiagramContent content(built(diagrams.at(folioIndex)), false); for (Conductor *c : content.conductors(DiagramContent::AnyConductor)) list << c->uuid().toString(); return list; @@ -1152,12 +1202,12 @@ QStringList QetScriptApi::conductorUuids(int folioIndex) const QStringList QetScriptApi::conductorEnds(int folioIndex, const QString &uuid) const { if (!m_project) return {}; - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return {}; const QUuid wanted(uuid); if (wanted.isNull()) return {}; - DiagramContent content(diagrams.at(folioIndex), false); + DiagramContent content(built(diagrams.at(folioIndex)), false); for (Conductor *c : content.conductors(DiagramContent::AnyConductor)) if (c->uuid() == wanted) return {describeEnd(c->terminal1), describeEnd(c->terminal2)}; @@ -1213,16 +1263,26 @@ bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUu Conductor *conductor = findConductor(folioIndex, elementUuid, terminalIndex, caller); if (!conductor) return false; - //Captured before the pushes below: QPropertyUndoCommand applies on - //push, so reading the properties afterwards would already give back - //@a properties. - const ConductorProperties before = conductor->properties(); - ConductorProperties properties = before; + ConductorProperties properties = conductor->properties(); if (!setConductorPropertyValue(properties, property, value)) { log(QStringLiteral("qet.%1: '%2' is not a valid value for %3") .arg(caller, value, property)); return false; } + return applyConductorProperties(conductor, properties); +} + +/** + @brief QetScriptApi::applyConductorProperties + Give @p conductor and every wire at its potential the properties + @p properties, one undo step +*/ +bool QetScriptApi::applyConductorProperties(Conductor *conductor, const ConductorProperties &properties) +{ + //Captured before the pushes below: QPropertyUndoCommand applies on + //push, so reading the properties afterwards would already give back + //@a properties. + const ConductorProperties before = conductor->properties(); if (properties == before) return true; // already so QSet potential = conductor->relatedPotentialConductors(true); @@ -1259,6 +1319,174 @@ bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUu return true; } +namespace { + //The wire properties a .qetz's database holds in its own columns, which + //nothing on a folio computes from but a wire's number formula +const QStringList &storedConductorPropertyNames() +{ + static const QStringList names { + QStringLiteral("num"), QStringLiteral("function"), QStringLiteral("bus"), + QStringLiteral("cable"), QStringLiteral("tension_protocol"), + QStringLiteral("conductor_color"), QStringLiteral("conductor_section")}; + return names; +} + + //true if @p a and @p b differ only in storedConductorPropertyNames() +bool differOnlyInStored(const ConductorProperties &a, const ConductorProperties &b) +{ + QHash left, right; + for (const auto &attribute : a.attributes()) left.insert(attribute.first, attribute.second); + for (const auto &attribute : b.attributes()) right.insert(attribute.first, attribute.second); + QSet names(left.keyBegin(), left.keyEnd()); + for (const QString &name : right.keys()) names.insert(name); + for (const QString &name : std::as_const(names)) + if (left.value(name) != right.value(name) && !storedConductorPropertyNames().contains(name)) + return false; + return true; +} + + //Wires given their properties by uuid, built or not (lazy folios): + //projectDataBase::setPlacedConductorProperties() +class ChangeStoredConductorPropertiesCommand : public QUndoCommand +{ + public: + ChangeStoredConductorPropertiesCommand(QETProject *project, + const QHash> &changes) : + m_project(project), m_changes(changes) + { setText(QObject::tr("Edit the conductor properties")); } + void undo() override { apply(false); } + void redo() override { apply(true); } + private: + void apply(bool forward) { + if (!m_project) return; + for (auto it = m_changes.cbegin() ; it != m_changes.cend() ; ++it) + m_project->dataBase()->setPlacedConductorProperties( + it.key(), forward ? it->second : it->first); + } + QPointer m_project; + QHash> m_changes; +}; +} + +/** + @brief QetScriptApi::findConductorByUuid + @return the wire @p conductorUuid of the folio @p folioIndex, the folio + built, or nullptr (said in the log) +*/ +Conductor *QetScriptApi::findConductorByUuid(int folioIndex, const QString &conductorUuid, + const QString &caller) +{ + if (!m_project || folioIndex < 0 || folioIndex >= m_project->folios().count()) { + log(QStringLiteral("qet.%1: no folio at index %2").arg(caller).arg(folioIndex)); + return nullptr; + } + const QUuid wanted(conductorUuid); + for (Conductor *c : built(m_project->folios().at(folioIndex))->conductors()) + if (c->uuid() == wanted) return c; + log(QStringLiteral("qet.%1: no conductor %2 on folio %3").arg(caller, conductorUuid).arg(folioIndex)); + return nullptr; +} + +/** + @brief QetScriptApi::unbuiltConductorChange + Change the property @p property of the wire @p wire, on the folio + @p folioIndex not built (lazy folios), and of every wire at its potential, + through the project database, as applyConductorProperties() would: when + they are all on folios not built, none is numbered by a formula, and what + changes in each is only what the .qetz database holds + (storedConductorPropertyNames()) -- nothing then is computed from it. + @return true if done; false for the folio to be built, @p refused set when + the value itself is not one +*/ +bool QetScriptApi::unbuiltConductorChange(int folioIndex, const QUuid &wire, const QString &property, + const QString &value, bool *refused) +{ + *refused = false; + projectDataBase *database = m_project->dataBase(); + Diagram *folio = m_project->folios().at(folioIndex); + QUuid on; + if (folio->isLoaded() || !database->rowsFromDocument() || !storedConductorPropertyNames().contains(property) + || !database->unbuiltWire(wire, &on) || on != folio->uuid()) { + return false; + } + ConductorProperties properties = database->conductorProperties(wire); + if (!setConductorPropertyValue(properties, property, value)) { + *refused = true; + return false; + } + if (!properties.m_formula.isEmpty()) { + return false; + } + const auto potential = database->potentialWires(wire); + if (!potential) { + return false; + } + QHash> changes; + for (const QUuid &other : *potential) { + if (!database->unbuiltWire(other)) return false; + const ConductorProperties old = database->conductorProperties(other); + if (!differOnlyInStored(old, properties)) return false; + if (!(old == properties)) changes.insert(other, {old, properties}); + } + if (!changes.isEmpty()) + m_project->undoStack()->push(new ChangeStoredConductorPropertiesCommand(m_project, changes)); + return true; +} + +QString QetScriptApi::conductorPropertyByUuid(int folioIndex, const QString &conductorUuid, + const QString &property) const +{ + if (!m_project || folioIndex < 0 || folioIndex >= m_project->folios().count()) return QString(); + Diagram *folio = m_project->folios().at(folioIndex); + projectDataBase *database = m_project->dataBase(); + QUuid on; + if (!folio->isLoaded() && database->rowsFromDocument() + && database->unbuiltWire(QUuid(conductorUuid), &on) && on == folio->uuid()) + return conductorPropertyValue(database->conductorProperties(QUuid(conductorUuid)), property); + auto *self = const_cast(this); + Conductor *conductor = self->findConductorByUuid(folioIndex, conductorUuid, + QStringLiteral("conductorPropertyByUuid")); + return conductor ? conductorPropertyValue(conductor->properties(), property) : QString(); +} + +bool QetScriptApi::setConductorPropertyByUuid(int folioIndex, const QString &conductorUuid, + const QString &property, const QString &value) +{ + if (!m_project) return false; + const QString caller = QStringLiteral("setConductorPropertyByUuid"); + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.%1: project is read-only").arg(caller)); + return false; + } + if (!conductorPropertyNames().contains(property)) { + log(QStringLiteral("qet.%1: unknown property '%2'; expected one of %3") + .arg(caller, property, conductorPropertyNames().join(QStringLiteral(", ")))); + return false; + } + if (folioIndex < 0 || folioIndex >= m_project->folios().count()) { + log(QStringLiteral("qet.%1: no folio at index %2").arg(caller).arg(folioIndex)); + return false; + } + //The folio left unbuilt when the change can go through the project + //database (stage D5) + bool refused = false; + if (unbuiltConductorChange(folioIndex, QUuid(conductorUuid), property, value, &refused)) + return true; + if (refused) { + log(QStringLiteral("qet.%1: '%2' is not a valid value for %3").arg(caller, value, property)); + return false; + } + Conductor *conductor = findConductorByUuid(folioIndex, conductorUuid, caller); + if (!conductor) return false; + ConductorProperties properties = conductor->properties(); + if (!setConductorPropertyValue(properties, property, value)) { + log(QStringLiteral("qet.%1: '%2' is not a valid value for %3") + .arg(caller, value, property)); + return false; + } + return applyConductorProperties(conductor, properties); +} + /** @brief QetScriptApi::conductorSegments List the drawn path of the conductor on a terminal, one line per @@ -1814,18 +2042,18 @@ QList sortedByPosition(const QSet &items) QList QetScriptApi::sortedTexts(int folioIndex) const { if (!m_project) return {}; - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return {}; - DiagramContent content(diagrams.at(folioIndex), false); + DiagramContent content(built(diagrams.at(folioIndex)), false); return sortedByPosition(content.m_text_fields); } QList QetScriptApi::sortedShapes(int folioIndex) const { if (!m_project) return {}; - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return {}; - DiagramContent content(diagrams.at(folioIndex), false); + DiagramContent content(built(diagrams.at(folioIndex)), false); return sortedByPosition(content.m_shapes); } @@ -1870,9 +2098,9 @@ int QetScriptApi::addText(int folioIndex, const QString &text, double x, double log(QStringLiteral("qet.addText: project is read-only")); return -1; } - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1; - Diagram *diagram = diagrams.at(folioIndex); + Diagram *diagram = built(diagrams.at(folioIndex)); auto *item = new IndependentTextItem(); item->setPlainText(text); @@ -1956,7 +2184,7 @@ bool QetScriptApi::deleteText(int folioIndex, int textIndex) } IndependentTextItem *item = findText(folioIndex, textIndex, QStringLiteral("deleteText")); if (!item) return false; - Diagram *diagram = m_project->diagrams().at(folioIndex); + Diagram *diagram = built(m_project->folios().at(folioIndex)); DiagramContent content; content.m_text_fields << item; @@ -1996,7 +2224,7 @@ int QetScriptApi::addShape(int folioIndex, const QString &type, log(QStringLiteral("qet.addShape: project is read-only")); return -1; } - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1; QetShapeItem::ShapeType shape_type; @@ -2011,7 +2239,7 @@ int QetScriptApi::addShape(int folioIndex, const QString &type, return -1; } - Diagram *diagram = diagrams.at(folioIndex); + Diagram *diagram = built(diagrams.at(folioIndex)); auto *shape = new QetShapeItem(QPointF(x1, y1), QPointF(x2, y2), shape_type); diagram->undoStack().push(new AddGraphicsObjectCommand(shape, diagram, QPointF(0, 0))); return sortedShapes(folioIndex).indexOf(shape); @@ -2143,7 +2371,7 @@ bool QetScriptApi::deleteShape(int folioIndex, int shapeIndex) .arg(folioIndex).arg(list.count()).arg(shapeIndex)); return false; } - Diagram *diagram = m_project->diagrams().at(folioIndex); + Diagram *diagram = built(m_project->folios().at(folioIndex)); DiagramContent content; content.m_shapes << list.at(shapeIndex); @@ -2250,7 +2478,7 @@ int QetScriptApi::addPolygon(int folioIndex, const QVariantList &points, bool cl log(QStringLiteral("qet.addPolygon: project is read-only")); return -1; } - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1; if (points.count() < 2) { log(QStringLiteral("qet.addPolygon: at least 2 points are required, got %1") @@ -2264,7 +2492,7 @@ int QetScriptApi::addPolygon(int folioIndex, const QVariantList &points, bool cl return -1; } - Diagram *diagram = diagrams.at(folioIndex); + Diagram *diagram = built(diagrams.at(folioIndex)); auto *shape = new QetShapeItem(poly.first(), poly.last(), QetShapeItem::Polygon); shape->setPolygon(poly); shape->setClosed(closed); @@ -2348,7 +2576,7 @@ int QetScriptApi::addPath(int folioIndex, const QVariantList &nodes, bool closed log(QStringLiteral("qet.addPath: project is read-only")); return -1; } - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1; if (nodes.count() < 2) { log(QStringLiteral("qet.addPath: at least 2 nodes are required, got %1").arg(nodes.count())); @@ -2365,7 +2593,7 @@ int QetScriptApi::addPath(int folioIndex, const QVariantList &nodes, bool closed path_nodes << node; } - Diagram *diagram = diagrams.at(folioIndex); + Diagram *diagram = built(diagrams.at(folioIndex)); auto *shape = new QetShapeItem(path_nodes.first().anchor, path_nodes.last().anchor, QetShapeItem::Path); shape->setPathNodes(path_nodes); @@ -2609,7 +2837,7 @@ bool QetScriptApi::deleteConductor(int folioIndex, const QString &elementUuid, i } Conductor *conductor = findConductor(folioIndex, elementUuid, terminalIndex, caller); if (!conductor) return false; - Diagram *diagram = m_project->diagrams().at(folioIndex); + Diagram *diagram = built(m_project->folios().at(folioIndex)); DiagramContent content; content.m_conductors_to_move << conductor; @@ -2630,10 +2858,10 @@ bool QetScriptApi::removeFolio(int folioIndex) log(QStringLiteral("qet.removeFolio: project is read-only")); return false; } - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return false; m_project->undoStack()->push(new RemoveDiagramCommand(m_project, diagrams.at(folioIndex))); - return m_project->diagrams().count() == diagrams.count() - 1; + return m_project->folios().count() == diagrams.count() - 1; } namespace { @@ -2667,7 +2895,7 @@ const QStringList &titleBlockFieldNames() QString QetScriptApi::folioProperty(int folioIndex, const QString &property) const { if (!m_project) return QString(); - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString(); Diagram *diagram = diagrams.at(folioIndex); if (property == QLatin1String("template")) @@ -2692,7 +2920,7 @@ bool QetScriptApi::setFolioProperty(int folioIndex, const QString &property, con log(QStringLiteral("qet.setFolioProperty: project is read-only")); return false; } - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return false; Diagram *diagram = diagrams.at(folioIndex); @@ -2985,9 +3213,9 @@ bool QetScriptApi::sortTerminalStrip(int stripIndex) QList QetScriptApi::sortedTables(int folioIndex) const { if (!m_project) return {}; - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return {}; - DiagramContent content(diagrams.at(folioIndex), false); + DiagramContent content(built(diagrams.at(folioIndex)), false); return sortedByPosition(content.m_tables); } @@ -3047,7 +3275,7 @@ int QetScriptApi::addTable(int folioIndex, const QString &kind, const QString &n log(QStringLiteral("qet.%1: project is read-only").arg(caller)); return -1; } - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) { log(QStringLiteral("qet.%1: no folio at index %2").arg(caller).arg(folioIndex)); return -1; @@ -3116,7 +3344,7 @@ int QetScriptApi::addTable(int folioIndex, const QString &kind, const QString &n dialog.setAddNewTableToNewDiagram(false); const QList before = sortedTables(folioIndex); - QetGraphicsTableFactory::create(diagrams.at(folioIndex), &dialog); + QetGraphicsTableFactory::create(built(diagrams.at(folioIndex)), &dialog); const QList after = sortedTables(folioIndex); for (QetGraphicsTableItem *t : after) { if (!before.contains(t)) return after.indexOf(t); @@ -3132,9 +3360,9 @@ bool QetScriptApi::deleteTable(int folioIndex, int tableIndex) log(QStringLiteral("qet.deleteTable: project is read-only")); return false; } - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return false; - Diagram *diagram = diagrams.at(folioIndex); + Diagram *diagram = built(diagrams.at(folioIndex)); const QList all = sortedTables(folioIndex); if (tableIndex < 0 || tableIndex >= all.count()) { @@ -3408,7 +3636,7 @@ bool QetScriptApi::useConductorAutoNum(int folioIndex, const QString &name) log(QStringLiteral("qet.useConductorAutoNum: project is read-only")); return false; } - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return false; if (!name.isEmpty() && !m_project->conductorAutoNum().contains(name)) { log(QStringLiteral("qet.useConductorAutoNum: no conductor auto-numbering named '%1'").arg(name)); @@ -3422,9 +3650,9 @@ bool QetScriptApi::useConductorAutoNum(int folioIndex, const QString &name) QList QetScriptApi::sortedImages(int folioIndex) const { if (!m_project) return {}; - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return {}; - DiagramContent content(diagrams.at(folioIndex), false); + DiagramContent content(built(diagrams.at(folioIndex)), false); return sortedByPosition(content.m_images); } @@ -3518,7 +3746,7 @@ int QetScriptApi::addImage(int folioIndex, const QString &filePath, double x, do log(QStringLiteral("qet.addImage: project is read-only")); return -1; } - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1; // Same checks as a picture dropped on a folio: a regular file of at @@ -3530,7 +3758,7 @@ int QetScriptApi::addImage(int folioIndex, const QString &filePath, double x, do return -1; } - Diagram *diagram = diagrams.at(folioIndex); + Diagram *diagram = built(diagrams.at(folioIndex)); auto *item = new DiagramImageItem(QPixmap::fromImage(image)); diagram->undoStack().push(new AddGraphicsObjectCommand(item, diagram, QPointF(x, y))); return sortedImages(folioIndex).indexOf(item); @@ -3646,7 +3874,7 @@ bool QetScriptApi::deleteImage(int folioIndex, int imageIndex) .arg(folioIndex).arg(list.count()).arg(imageIndex)); return false; } - Diagram *diagram = m_project->diagrams().at(folioIndex); + Diagram *diagram = built(m_project->folios().at(folioIndex)); DiagramContent content; content.m_images << list.at(imageIndex); diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, content)); @@ -3702,7 +3930,7 @@ int QetScriptApi::addPdfPage(int folioIndex, const QString &pdfPath, int pageNum log(QStringLiteral("qet.addPdfPage: project is read-only")); return -1; } - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1; if (dpi <= 0) { log(QStringLiteral("qet.addPdfPage: dpi must be positive, got %1").arg(dpi)); @@ -3751,7 +3979,7 @@ int QetScriptApi::addPdfPage(int folioIndex, const QString &pdfPath, int pageNum painter.drawImage(0, 0, rendered); painter.end(); - Diagram *diagram = diagrams.at(folioIndex); + Diagram *diagram = built(diagrams.at(folioIndex)); auto *item = new DiagramImageItem(QPixmap::fromImage(background)); diagram->undoStack().push(new AddGraphicsObjectCommand(item, diagram, QPointF(x, y))); return sortedImages(folioIndex).indexOf(item); @@ -3975,7 +4203,7 @@ bool QetScriptApi::deleteElementText(int folioIndex, const QString &elementUuid, DynamicElementTextItem *t = findElementText(folioIndex, elementUuid, textIndex, QStringLiteral("deleteElementText")); if (!t) return false; - Diagram *diagram = m_project->diagrams().at(folioIndex); + Diagram *diagram = built(m_project->folios().at(folioIndex)); DiagramContent content; content.m_element_texts << t; diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, content)); @@ -4205,7 +4433,7 @@ QStringList QetScriptApi::duplicateElements(int fromFolioIndex, const QStringLis log(QStringLiteral("qet.%1: project is read-only").arg(caller)); return created; } - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (fromFolioIndex < 0 || fromFolioIndex >= diagrams.count() || toFolioIndex < 0 || toFolioIndex >= diagrams.count()) { log(QStringLiteral("qet.%1: folio index out of range").arg(caller)); @@ -4215,8 +4443,8 @@ QStringList QetScriptApi::duplicateElements(int fromFolioIndex, const QStringLis log(QStringLiteral("qet.%1: no elements named").arg(caller)); return created; } - Diagram *source = diagrams.at(fromFolioIndex); - Diagram *target = diagrams.at(toFolioIndex); + Diagram *source = built(diagrams.at(fromFolioIndex)); + Diagram *target = built(diagrams.at(toFolioIndex)); QList chosen; for (const QString &uuid : elementUuids) { @@ -4300,7 +4528,7 @@ const QStringList &folioBorderNames() QString QetScriptApi::folioBorder(int folioIndex, const QString &property) const { if (!m_project) return QString(); - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString(); const BorderProperties b = diagrams.at(folioIndex)->border_and_titleblock.exportBorder(); if (property == QLatin1String("columns")) return QString::number(b.columns_count); @@ -4439,7 +4667,7 @@ bool QetScriptApi::setFolioBorder(int folioIndex, const QString &property, const .arg(caller, property, folioBorderNames().join(QStringLiteral(", ")))); return false; } - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return false; Diagram *diagram = diagrams.at(folioIndex); @@ -4525,7 +4753,7 @@ QString QetScriptApi::conductorDefault(int folioIndex, const QString &property) if (folioIndex == -1) { p = m_project->defaultConductorProperties(); } else { - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString(); p = diagrams.at(folioIndex)->defaultConductorProperties; } @@ -4551,7 +4779,7 @@ bool QetScriptApi::setConductorDefault(int folioIndex, const QString &property, } Diagram *diagram = nullptr; if (folioIndex != -1) { - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return false; diagram = diagrams.at(folioIndex); } @@ -4660,12 +4888,12 @@ QVariantMap QetScriptApi::terminalPosition(int folioIndex, const QString &elemen QVariantList QetScriptApi::conductorPath(int folioIndex, const QString &conductorUuid) const { if (!m_project) return {}; - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return {}; const QUuid wanted(conductorUuid); if (wanted.isNull()) return {}; - DiagramContent content(diagrams.at(folioIndex), false); + DiagramContent content(built(diagrams.at(folioIndex)), false); for (Conductor *c : content.conductors(DiagramContent::AnyConductor)) { if (c->uuid() != wanted) continue; QVariantList points; @@ -4700,14 +4928,14 @@ int QetScriptApi::insertFolio(int position) log(QStringLiteral("qet.insertFolio: project is read-only")); return -1; } - if (position < 0 || position > m_project->diagrams().count()) { + if (position < 0 || position > m_project->folios().count()) { log(QStringLiteral("qet.insertFolio: position %1 is outside 0..%2") - .arg(position).arg(m_project->diagrams().count())); + .arg(position).arg(m_project->folios().count())); return -1; } Diagram *diagram = m_project->addNewDiagram(position); if (!diagram) return -1; - return m_project->diagrams().indexOf(diagram); + return m_project->folios().indexOf(diagram); } /** @@ -4787,7 +5015,7 @@ int QetScriptApi::addFolio() } Diagram *diagram = m_project->addNewDiagram(); if (!diagram) return -1; - return m_project->diagrams().indexOf(diagram); + return m_project->folios().indexOf(diagram); } bool QetScriptApi::setFolioTitle(int folioIndex, const QString &title) @@ -4797,7 +5025,7 @@ bool QetScriptApi::setFolioTitle(int folioIndex, const QString &title) log(QStringLiteral("qet.setFolioTitle: project is read-only")); return false; } - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return false; Diagram *diagram = diagrams.at(folioIndex); @@ -5229,7 +5457,15 @@ QVariantList QetScriptApi::checkContinuity(int folioIndex) bool QetScriptApi::selectElement(const QString &elementUuid) { if (!m_project) return false; - for (Diagram *diagram : m_project->diagrams()) { + //The folio the project database places it on, built; every folio + //when it places it on none + QList folios; + const QSet on = m_project->dataBase()->folioUuidsOfElements({QUuid(elementUuid)}); + for (Diagram *diagram : m_project->folios()) + if (on.contains(diagram->uuid())) folios << built(diagram); + if (folios.isEmpty()) + folios = m_project->diagrams(); + for (Diagram *diagram : std::as_const(folios)) { DiagramContent content(diagram, false); for (Element *elmt : std::as_const(content.m_elements)) { if (elmt->uuid().toString() == elementUuid) { @@ -5245,7 +5481,7 @@ QStringList QetScriptApi::selectedElements(int folioIndex) const { QStringList list; if (!m_project) return list; - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return list; for (QGraphicsItem *item : diagrams.at(folioIndex)->selectedItems()) { if (item->type() == Element::Type) { @@ -5259,7 +5495,7 @@ QStringList QetScriptApi::selectedElements(int folioIndex) const void QetScriptApi::deselectAll(int folioIndex) { if (!m_project) return; - const QList diagrams = m_project->diagrams(); + const QList diagrams = m_project->folios(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return; diagrams.at(folioIndex)->clearSelection(); } diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index f6b8b0c0e..9d0c9c7b7 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -24,11 +24,17 @@ #include #include +#include + +#include "../diagramcontext.h" + class QETProject; +class Diagram; class DiagramView; class Element; class Terminal; class Conductor; +class ConductorProperties; class IndependentTextItem; class QetShapeItem; class DiagramImageItem; @@ -435,6 +441,11 @@ class QetScriptApi : public QObject Q_INVOKABLE bool setConductorProperty(int folioIndex, const QString &elementUuid, int terminalIndex, const QString &property, const QString &value); + // The same, the wire named by its uuid (conductorUuids()) + Q_INVOKABLE QString conductorPropertyByUuid(int folioIndex, const QString &conductorUuid, + const QString &property) const; + Q_INVOKABLE bool setConductorPropertyByUuid(int folioIndex, const QString &conductorUuid, + const QString &property, const QString &value); // -- a conductor's own drawn path, not the whole potential's // properties above -- one conductor only, addressed the same way -- @@ -642,6 +653,12 @@ class QetScriptApi : public QObject private: bool runFlag(const QString &flag, const QStringList &args); + Diagram *built(Diagram *folio) const; + Conductor *findConductorByUuid(int folioIndex, const QString &conductorUuid, const QString &caller); + bool applyConductorProperties(Conductor *conductor, const ConductorProperties &properties); + bool unbuiltConductorChange(int folioIndex, const QUuid &wire, const QString &property, + const QString &value, bool *refused); + std::optional unbuiltInformation(int folioIndex, const QString &elementUuid) const; Element *findElement(int folioIndex, const QString &elementUuid) const; Terminal *findTerminal(int folioIndex, const QString &elementUuid, int terminalIndex, const QString &caller); diff --git a/sources/ui/terminalnumberingdialog.cpp b/sources/ui/terminalnumberingdialog.cpp index 6e30d32cc..89811db79 100644 --- a/sources/ui/terminalnumberingdialog.cpp +++ b/sources/ui/terminalnumberingdialog.cpp @@ -13,6 +13,16 @@ #include #include +namespace { + //The folios holding a terminal, built: a folio not built yet + //(lazy folios) holding none is not built for this +QList terminalFolios(QETProject *project) +{ + project->buildFoliosHolding(ElementData::Terminal, false); + return project->builtFolios(); +} +} + /** * @brief TerminalNumberingDialog::TerminalNumberingDialog * Constructor @@ -31,7 +41,7 @@ TerminalNumberingDialog::TerminalNumberingDialog(QWidget *parent, QETProject *pr // Collect all unique terminal strip prefixes from the project if (project) { QSet prefixes; - foreach (Diagram *diagram, project->diagrams()) { + foreach (Diagram *diagram, terminalFolios(project)) { foreach (QGraphicsItem *qgi, diagram->items()) { if (Element *elmt = qgraphicsitem_cast(qgi)) { if (elmt->elementData().m_type == ElementData::Terminal) { @@ -153,7 +163,7 @@ QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const QList termList; // 2. Collect all terminals from all folios in the project - foreach (Diagram *diagram, project->diagrams()) { + foreach (Diagram *diagram, terminalFolios(project)) { int fIndex = diagram->folioIndex(); foreach (QGraphicsItem *qgi, diagram->items()) { if (Element *elmt = qgraphicsitem_cast(qgi)) { diff --git a/sources/undocommand/changeelementinformationcommand.cpp b/sources/undocommand/changeelementinformationcommand.cpp index bf7045b9d..7a76220e5 100644 --- a/sources/undocommand/changeelementinformationcommand.cpp +++ b/sources/undocommand/changeelementinformationcommand.cpp @@ -119,3 +119,27 @@ void ChangeElementInformationCommand::updateProjectDB() elmt->diagram()->project()->dataBase()->elementInfoChanged(list_); } } + +ChangeStoredElementInformationCommand::ChangeStoredElementInformationCommand( + QETProject *project, const QUuid &element, const DiagramContext &old_info, + const DiagramContext &new_info, QUndoCommand *parent) : + QUndoCommand(parent), + m_project(project), + m_element(element), + m_old_info(old_info), + m_new_info(new_info) +{ + setText(QObject::tr("Change the information of the element")); +} + +void ChangeStoredElementInformationCommand::undo() +{ + if (m_project) + m_project->dataBase()->setPlacedElementInformation(m_element, m_old_info); +} + +void ChangeStoredElementInformationCommand::redo() +{ + if (m_project) + m_project->dataBase()->setPlacedElementInformation(m_element, m_new_info); +} diff --git a/sources/undocommand/changeelementinformationcommand.h b/sources/undocommand/changeelementinformationcommand.h index 6e103ebf6..9f6bcee24 100644 --- a/sources/undocommand/changeelementinformationcommand.h +++ b/sources/undocommand/changeelementinformationcommand.h @@ -21,8 +21,11 @@ #include "../diagramcontext.h" #include +#include +#include class Element; +class QETProject; /** @brief The ChangeElementInformationCommand class @@ -52,4 +55,26 @@ class ChangeElementInformationCommand : public QUndoCommand QMap, QPair> m_map; }; +/** + @brief The ChangeStoredElementInformationCommand class + Change the information of a symbol given by its uuid, built or not + (lazy folios): through the symbol when it is built, through the + project database when it is not (projectDataBase::canEditUnbuilt()). +*/ +class ChangeStoredElementInformationCommand : public QUndoCommand +{ + public: + ChangeStoredElementInformationCommand(QETProject *project, const QUuid &element, + const DiagramContext &old_info, + const DiagramContext &new_info, + QUndoCommand *parent = nullptr); + void undo() override; + void redo() override; + + private: + QPointer m_project; + QUuid m_element; + DiagramContext m_old_info, m_new_info; +}; + #endif // CHANGEELEMENTINFORMATIONCOMMAND_H diff --git a/sources/wiringlistexport.cpp b/sources/wiringlistexport.cpp index c56b6fc76..8de281782 100644 --- a/sources/wiringlistexport.cpp +++ b/sources/wiringlistexport.cpp @@ -1,6 +1,9 @@ #include "wiringlistexport.h" #include "qetproject.h" #include "diagram.h" +#include "dataBase/projectdatabase.h" +#include "ElementsCollection/xmlelementcollection.h" +#include "qetgraphicsitem/element.h" #include #include #include @@ -28,8 +31,8 @@ QString WiringListExport::findDiagramFolio(const QDomElement &diagramElem) const // The folio number as the folio shows it ("3/12"). The saved "folio" // attribute is its template ("%id/%total"). const int index = diagramElem.attribute("order").toInt(); - if (m_project && index >= 1 && index <= m_project->diagrams().size()) { - return m_project->diagrams().at(index - 1)->border_and_titleblock + if (m_project && index >= 1 && index <= m_project->folios().size()) { + return m_project->folios().at(index - 1)->border_and_titleblock .titleblockInformation().value("folio").toString(); } if (diagramElem.hasAttribute("folio")) return diagramElem.attribute("folio"); @@ -48,6 +51,89 @@ QDomElement WiringListExport::climbToDiagram(QDomNode node) const return QDomElement(); } +/** + The project as the export reads it: its symbol collection and its folios + in order, each with its "order". A folio not built (lazy folios) is + not built for it: its XML is copied, and what QElectroTech computes for + its wires' ends -- the labels of the symbols they end on and of their + partners, the terminals' names -- is filled in from the symbols built + (on built folios or alone, as partners) and from the project database. +*/ +QDomDocument WiringListExport::projectDocument() const +{ + QDomDocument document; + QDomElement root = document.createElement(QStringLiteral("project")); + document.appendChild(root); + if (XmlElementCollection *collection = m_project->embeddedElementCollection()) + root.appendChild(document.importNode(collection->root(), true)); + + //A folio whose partners built alone were linked or unlinked since + //is not what its XML says: built, as saving builds it + for (Diagram *diagram : m_project->folios()) + if (!diagram->isLoaded() && diagram->partialChanged()) + m_project->buildFolios({diagram}); + + QHash built; + for (Diagram *diagram : m_project->folios()) { + if (diagram->isLoaded()) { + for (Element *element : diagram->elements()) built.insert(element->uuid(), element); + } else { + const QHash partial = diagram->partialElements(); + for (auto it = partial.cbegin() ; it != partial.cend() ; ++it) built.insert(it.key(), it.value()); + } + } + projectDataBase *database = m_project->dataBase(); + auto label = [&built, database](const QUuid &uuid) { + Element *element = built.value(uuid); + return element ? element->actualLabel() : database->elementLabel(uuid); + }; + //Conductor::toXml(): the label of the last of the symbol's partners. + //A symbol still waiting for a partner not built has its links as + //saved, in the database + auto linked = [&built, database, &label](const QUuid &uuid) -> std::optional { + Element *element = built.value(uuid); + if (element && element->pendingLinks().isEmpty()) { + if (element->linkedElements().isEmpty()) return std::nullopt; + return element->linkedElements().last()->actualLabel(); + } + const QList links = database->linkedSymbols(uuid); + if (links.isEmpty()) return std::nullopt; + return label(links.last()); + }; + + const QList folios = m_project->folios(); + for (int i = 0 ; i < folios.size() ; ++i) { + Diagram *diagram = folios.at(i); + QDomElement folio; + const bool is_built = diagram->isLoaded(); + folio = document.importNode(is_built ? m_project->folioXml(diagram).documentElement() + : diagram->deferredXml(), true).toElement(); + //A built folio's wires describe their ends but a partner not + //built yet; a folio not built has none of it in its part (.qetz) + //or as read, before any change (.qet) + QDomNodeList wires = folio.elementsByTagName(QStringLiteral("conductor")); + for (int w = 0 ; w < wires.size() ; ++w) { + QDomElement wire = wires.at(w).toElement(); + for (const QString end : {QStringLiteral("1"), QStringLiteral("2")}) { + const QUuid element(wire.attribute(QStringLiteral("element") + end)); + if (element.isNull()) continue; //an end saved the older way + if (const auto partner = linked(element)) + wire.setAttribute(QStringLiteral("element") + end + QStringLiteral("_linked"), *partner); + else + wire.removeAttribute(QStringLiteral("element") + end + QStringLiteral("_linked")); + if (is_built) continue; + wire.setAttribute(QStringLiteral("element") + end + QStringLiteral("_label"), label(element)); + wire.setAttribute(QStringLiteral("terminalname") + end, + database->terminalName(QUuid(wire.attribute(QStringLiteral("terminal") + end)), + element)); + } + } + folio.setAttribute(QStringLiteral("order"), i + 1); + root.appendChild(folio); + } + return document; +} + QMap WiringListExport::collectElementsInfo(const QDomElement &root) const { QMap infoMap; @@ -191,7 +277,7 @@ QString WiringListExport::toCsvString() const { if (!m_project) return QString(); - QDomDocument doc = m_project->toXml(); + QDomDocument doc = projectDocument(); if (doc.isNull()) return QString(); QSet conductorDefinitionTypes; diff --git a/sources/wiringlistexport.h b/sources/wiringlistexport.h index 8f1696b29..025249e55 100644 --- a/sources/wiringlistexport.h +++ b/sources/wiringlistexport.h @@ -6,6 +6,7 @@ #include #include #include +#include class QETProject; class QWidget; @@ -60,6 +61,7 @@ private: QString normalizeUuid(const QString &u) const; QString findDiagramFolio(const QDomElement &diagramElem) const; QDomElement climbToDiagram(QDomNode node) const; + QDomDocument projectDocument() const; QMap collectElementsInfo(const QDomElement &root) const; QList collectConductors(const QDomElement &root) const; diff --git a/tests/modal-quit-regression/run.sh b/tests/modal-quit-regression/run.sh index 72638b9b9..2e7da7f10 100755 --- a/tests/modal-quit-regression/run.sh +++ b/tests/modal-quit-regression/run.sh @@ -33,7 +33,13 @@ # 2. let it run for a moment, then interrupt it -- the main thread is now # inside the dialog's nested loop, which is the only place the bug lives # (a deleteLater() posted before exec() started is not processed by it); -# 3. call QETApp::quitQET(), which closes every editor; +# 3. let it run on to the start of the loop's next QEventLoop::processEvents() +# on the main thread, and call QETApp::quitQET() there, which closes +# every editor. Not where the interrupt lands: that can be inside +# malloc(), whose lock a call into the program then waits on for ever +# (1 run in 20 timed out that way, on master too). Between two +# events the loop holds nothing, and QElectroTech's event loop +# watchdog wakes it every 50 ms, so the stop comes at once; # 4. let it run again: the unfixed build aborts within a second, the fixed # one keeps running until the test interrupts it. # @@ -104,14 +110,32 @@ if command -v nm >/dev/null; then fi SANDBOX="$(mktemp -d /tmp/qet-modal-quit.XXXXXX)" -cleanup() { [ "${KEEP_LOGS:-0}" = "1" ] || rm -rf "$SANDBOX"; } + +# SingleApplication keeps a System V shared memory block, keyed from a file in +# the temporary folder: 16 bits of the key vary. This run ends QElectroTech +# with gdb's kill, which leaves its block behind; blocks left so pile up, and +# once a new run's key falls on one, SingleApplication cannot start ("Unable to +# attach to shared memory block") -- 1 run in 40 with 976 of them. So the +# blocks this run leaves, attached to no process, are removed at the end, and +# a run that meets someone else's is tried again (below). +orphan_blocks() { + command -v ipcs >/dev/null || return 0 + ipcs -m 2>/dev/null | awk -v me="$(id -un)" 'NR > 3 && $3 == me && $6 == 0 { print $2 }' +} +BLOCKS_BEFORE=" $(orphan_blocks | tr '\n' ' ') " +cleanup() { + if command -v ipcrm >/dev/null; then + for id in $(orphan_blocks); do + case "$BLOCKS_BEFORE" in *" $id "*) ;; *) ipcrm -m "$id" 2>/dev/null ;; esac + done + fi + [ "${KEEP_LOGS:-0}" = "1" ] || rm -rf "$SANDBOX" +} trap cleanup EXIT # A unique binary path gives this run its own SingleApplication socket, so it # can neither be captured by nor capture a QElectroTech already running. A # symlink would not do: applicationFilePath() resolves it back to the real path. -TEST_BINARY="$SANDBOX/qelectrotech-modalquit" -cp "$BINARY" "$TEST_BINARY" || { echo "could not copy binary" >&2; exit 2; } # Keep the original file name. QElectroTech decides what to open from the # extension, so copying a .elmt to "project.qet" would quietly turn an @@ -141,9 +165,16 @@ end break QDialog::exec run delete +if \$_isvoid(\$_exitcode) printf "QET_TEST: dialog exec() entered\n" +else +printf "QET_TEST: exited before any dialog, code %d\n", \$_exitcode +end python qet_interrupt_later($SETTLE) continue +tbreak QEventLoop::processEvents thread 1 +continue +printf "QET_TEST: at the dialog's event loop\n" thread 1 set \$app = ((void* (*)(void))'QETApp::instance()')() call ((void (*)(void*))'QETApp::quitQET()')(\$app) @@ -162,14 +193,29 @@ EOF # the full watchdog period no matter how fast gdb finished: ten seconds of # work reported as two minutes, a stone's throw from the CTest timeout. GDB_TIMEOUT=120 -if command -v timeout >/dev/null; then - timeout --signal=KILL "$GDB_TIMEOUT" \ +# Each try under a binary path of its own: a new SingleApplication key. Tried +# again only when QElectroTech stopped before any dialog because +# SingleApplication met a block it could not use -- the scenario never ran, +# the code under test played no part. +for attempt in 1 2 3; do + TEST_BINARY="$SANDBOX/qelectrotech-modalquit-$attempt" + cp "$BINARY" "$TEST_BINARY" || { echo "could not copy binary" >&2; exit 2; } + if command -v timeout >/dev/null; then + timeout --signal=KILL "$GDB_TIMEOUT" \ + gdb -batch -x "$SANDBOX/scenario.gdb" --args "$TEST_BINARY" "$SANDBOX_PROJECT" \ + > "$SANDBOX/gdb.log" 2>&1 + else gdb -batch -x "$SANDBOX/scenario.gdb" --args "$TEST_BINARY" "$SANDBOX_PROJECT" \ - > "$SANDBOX/gdb.log" 2>&1 -else - gdb -batch -x "$SANDBOX/scenario.gdb" --args "$TEST_BINARY" "$SANDBOX_PROJECT" \ - > "$SANDBOX/gdb.log" 2>&1 -fi + > "$SANDBOX/gdb.log" 2>&1 + fi + if grep -q "QET_TEST: exited before any dialog" "$SANDBOX/gdb.log" \ + && grep -q "SingleApplication: Unable to" "$SANDBOX/gdb.log"; then + echo "note: try $attempt: SingleApplication could not set up its shared memory (a block left by another run); trying again" + cp "$SANDBOX/gdb.log" "$SANDBOX/gdb-try$attempt.log" + continue + fi + break +done log="$SANDBOX/gdb.log" if ! grep -q "QET_TEST: dialog exec() entered" "$log"; then diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index d76dc3d95..5a2cf3ed5 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -92,7 +92,7 @@ if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja") --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 + set_tests_properties(tst_lazyfolios PROPERTIES TIMEOUT 1800 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 @@ -1073,10 +1073,13 @@ endif() add_executable( tst_resaveunchanged - tst_resaveunchanged.cpp) + tst_resaveunchanged.cpp + ${QET_DIR}/sources/container/qetzip.cpp + ${QET_DIR}/sources/container/miniz/miniz.c) add_test(NAME tst_resaveunchanged COMMAND tst_resaveunchanged) add_dependencies(tst_resaveunchanged qelectrotech) -target_link_libraries(tst_resaveunchanged PRIVATE Qt::Test) +target_include_directories(tst_resaveunchanged PRIVATE ${QET_DIR}/sources) +target_link_libraries(tst_resaveunchanged PRIVATE Qt::Test SQLite3::SQLite3) target_compile_definitions(tst_resaveunchanged PRIVATE "QET_TEST_BINARY_PATH=\"$\"" "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") diff --git a/tests/qttest/lazy_folios_probe.cpp b/tests/qttest/lazy_folios_probe.cpp index 2ed2d085e..4e888e142 100644 --- a/tests/qttest/lazy_folios_probe.cpp +++ b/tests/qttest/lazy_folios_probe.cpp @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later -// Lazy folios (QET_LAZY_FOLIOS), +// Lazy folios (on by default, QET_EAGER_FOLIOS=1 for every folio built), // 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 @@ -23,14 +23,22 @@ #include "../../sources/qetgraphicsitem/element.h" #include "../../sources/autoNum/elementautonumschemecommand.h" #include "../../sources/elementprovider.h" +#include "../../sources/container/qetzip.h" +#include +#include #include "../../sources/TerminalStrip/terminalstrip.h" #include "../../sources/ui/elementpropertieswidget.h" #include "../../sources/conductorautonumerotation.h" +#include "../../sources/conductornumexport.h" +#include "../../sources/scripting/qetscriptapi.h" +#include "../../sources/wiringlistexport.h" +#include "../../sources/ui/terminalnumberingdialog.h" #include "../../sources/diagramcontent.h" #include "../../sources/diagramview.h" #include "../../sources/diagramevent/diagrameventaddpaste.h" #include #include "../../sources/qetgraphicsitem/conductor.h" +#include "../../sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.h" #include "../../sources/qetgraphicsitem/terminal.h" #include "../../sources/undocommand/changeelementinformationcommand.h" #include "../../sources/undocommand/changetitleblockcommand.h" @@ -41,6 +49,73 @@ #include static void check(bool value, const char *message) { if (!value) throw std::runtime_error(message); } +static QStringList sortedLines(QString xml, const QString &path); + + //A new revision on every folio (its title block's revision index and + //author), saved lazily and with every folio built: what saving builds, + //and whether the two saves hold the same. A change to folio + //@p label_folio's label (what formulas read) when it is not -1 +static void revisionEveryFolio(const QString &source, const QString &output, const QString &name, + const QString &path, int label_folio = -1) +{ + QString error; + qunsetenv("QET_EAGER_FOLIOS"); + QETProject revised(source); + const int unbuilt = revised.unloadedFolioCount(); + QetScriptApi api(&revised); + for (int i = 0 ; i < api.folioCount() ; ++i) { + check(api.setFolioProperty(i, "indexrev", "B"), "a folio's revision set"); + check(api.setFolioProperty(i, "author", "Revised"), "a folio's author set"); + } + if (label_folio >= 0) + check(api.setFolioProperty(label_folio, "folio", "X%id"), "a folio's label set"); + check(revised.unloadedFolioCount() == unbuilt, "setting the folios' title blocks builds none"); + int with_tables = 0; + for (Diagram *folio : revised.folios()) + if (!folio->isLoaded() && revised.unbuiltFolioHasTables(folio)) ++with_tables; + const bool qetz = QETProject::isContainerPath(source); + const QString lazy_path = output + "/" + name + (qetz ? ".qetz" : ".qet"); + if (qetz) check(revised.writeContainerFile(lazy_path, &error), "revised project saved"); + else { revised.setFilePath(lazy_path); check(revised.write().isOk(), "revised project saved"); } + const int built = unbuilt - revised.unloadedFolioCount(); + std::printf("%s: new revision on every folio, %d folios built by saving, %d hold a table%s\n", + qPrintable(name), built, with_tables, + label_folio >= 0 ? ", one label changed" : ""); + if (label_folio < 0) + check(built <= with_tables, "saving a new revision builds only the folios holding a table"); + else + check(revised.folios().at(label_folio)->isLoaded(), "a folio whose label changed is built to save"); + qputenv("QET_EAGER_FOLIOS", "1"); + const QString usual_path = output + "/" + name + "-usual" + (qetz ? ".qetz" : ".qet"); + { + QETProject usual(source); + QetScriptApi usual_api(&usual); + for (int i = 0 ; i < usual_api.folioCount() ; ++i) { + usual_api.setFolioProperty(i, "indexrev", "B"); + usual_api.setFolioProperty(i, "author", "Revised"); + } + if (label_folio >= 0) usual_api.setFolioProperty(label_folio, "folio", "X%id"); + if (qetz) check(usual.writeContainerFile(usual_path, &error), "revision saved when built"); + else { usual.setFilePath(usual_path); check(usual.write().isOk(), "revision saved when built"); } + } + QETProject lazy_back(lazy_path), usual_back(usual_path); + qunsetenv("QET_EAGER_FOLIOS"); + //What a save says of itself (file name, path, time) left out + auto lines = [&path](QETProject &project) { + QStringList list = sortedLines(project.toXml().toString(), path); + list.removeIf([](const QString &line) { return line.contains(QLatin1String("name=\"saved")); }); + return list; + }; + const QStringList lazy_lines = lines(lazy_back), usual_lines = lines(usual_back); + if (lazy_lines != usual_lines) { + int shown = 0; + for (const QString &line : usual_lines) + if (!lazy_lines.contains(line) && shown++ < 8) std::printf(" when built: %s\n", qPrintable(line.left(300))); + for (const QString &line : lazy_lines) + if (!usual_lines.contains(line) && shown++ < 16) std::printf(" lazily: %s\n", qPrintable(line.left(300))); + } + check(lazy_lines == usual_lines, "a new revision saved as when built"); +} //Every symbol's numbering, in a stable order static QStringList numbering(const QETProject &project) @@ -176,6 +251,55 @@ static QString saved(QETProject &project) return QETProject::spliceKeptFolios(document.toString(4), kept); } + //@p xml's lines in order, any uuid not in the file @p path "new": a + //pasted copy's and a new wire's are random, and a folio's symbols are + //saved in uuid order +static QStringList sortedLines(QString xml, const QString &path) +{ + QFile file(path); file.open(QIODevice::ReadOnly); + const QString before = QString::fromUtf8(file.readAll()); + static const QRegularExpression uuid_re(QStringLiteral("\\{[0-9a-f-]{36}\\}")); + for (auto it = uuid_re.globalMatch(QString(xml)) ; it.hasNext() ; ) { + const QString uuid = it.next().captured(0); + if (!before.contains(uuid)) xml.replace(uuid, QStringLiteral("new")); + } + QStringList list = xml.split(QLatin1Char('\n')); + list.sort(); + return list; +} + + //The saved links of a .qetz, symbol by symbol in their order: what a + //symbol saves of its partners, as its file holds it +static QStringList savedLinks(const QString &qetz, const QString &path) +{ + QFile file(qetz); file.open(QIODevice::ReadOnly); + QList entries; + QString error; + check(QetZip::fromBytes(file.readAll(), &entries, &error), "a .qetz read"); + QTemporaryDir dir; + const QString db_path = dir.filePath(QStringLiteral("project.sqlite")); + for (const QetZip::Entry &entry : entries) + if (entry.name == QLatin1String("project.sqlite")) { + QFile db(db_path); db.open(QIODevice::WriteOnly); db.write(entry.data); + } + QMap links; //symbol -> its partners in order + { + QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("saved_links")); + db.setDatabaseName(db_path); + check(db.open(), "a .qetz's database opened"); + QSqlQuery query(db); + check(query.exec(QStringLiteral("SELECT element_uuid, linked_uuid FROM link ORDER BY element_uuid, ord")), + "saved links read"); + while (query.next()) + links[query.value(0).toString()] << query.value(1).toString(); + } + QSqlDatabase::removeDatabase(QStringLiteral("saved_links")); + QStringList rows; + for (auto it = links.cbegin() ; it != links.cend() ; ++it) + rows << it.key() + QStringLiteral(": ") + it.value().join(QLatin1Char(',')); + return sortedLines(rows.join(QLatin1Char('\n')), path); +} + //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) @@ -240,7 +364,7 @@ int main(int argc, char **argv) qputenv("QET_LAZY_SAVE_KEPT", "1"); try { const QString output = QString::fromLocal8Bit(argv[3]); - qunsetenv("QET_LAZY_FOLIOS"); + qputenv("QET_EAGER_FOLIOS", "1"); //A file saved by this version: its folios carry their uuids, //which opening without building them needs const QString path = output + "/lazy_folios.qet"; @@ -267,9 +391,9 @@ int main(int argc, char **argv) QETProject eager(path); check(eager.state() == QETProject::Ok, "project opens"); { - qputenv("QET_LAZY_FOLIOS", "1"); + qunsetenv("QET_EAGER_FOLIOS"); QETProject lazy_strips(path); - qunsetenv("QET_LAZY_FOLIOS"); + qputenv("QET_EAGER_FOLIOS", "1"); auto strip_of = [](const QETProject &project) { for (TerminalStrip *strip : project.terminalStrip()) if (strip->name() == QStringLiteral("X99")) return strip->physicalTerminalCount(); @@ -290,7 +414,7 @@ int main(int argc, char **argv) } check(linked > 0, "the project has linked symbols"); - qputenv("QET_LAZY_FOLIOS", "1"); + qunsetenv("QET_EAGER_FOLIOS"); auto same = [&](QETProject &lazy, int i, const char *what) { Diagram *folio = lazy.folios().at(i); lazy.buildFolioToShow(folio); @@ -323,6 +447,24 @@ int main(int argc, char **argv) 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"); + revisionEveryFolio(path, output, "revised-qet", path); + { + //Wire numbers exported from the database filled from the + //document, as when built (D5) + QETProject exported(path); + check(ConductorNumExport(&exported).wiresNum() == ConductorNumExport(&eager).wiresNum(), + "wire numbers of a .qet exported as when built"); + const QString wiring = WiringListExport(&exported).toCsvString(), usual_wiring = WiringListExport(&eager).toCsvString(); + if (wiring != usual_wiring) { + const QStringList a = wiring.split('\n'), b = usual_wiring.split('\n'); + for (const QString &line : b) if (!a.contains(line)) std::printf(" wiring when built: %s\n", qPrintable(line)); + for (const QString &line : a) if (!b.contains(line)) std::printf(" wiring lazily: %s\n", qPrintable(line)); + std::printf(" %d lines lazily, %d when built\n", int(a.size()), int(b.size())); + } + check(wiring == usual_wiring, "wiring list of a .qet exported as when built"); + check(exported.unloadedFolioCount() == folios.size() - strip_folios.size(), + "exporting a .qet's wire numbers and wiring list builds no folio"); + } //A symbol placed on a folio, numbered by a scheme others follow QString location, title; int folio = -1; @@ -400,9 +542,9 @@ int main(int argc, char **argv) } { QETProject edited(path), usual(path); - qunsetenv("QET_LAZY_FOLIOS"); + qputenv("QET_EAGER_FOLIOS", "1"); QETProject usual_edited(path); - qputenv("QET_LAZY_FOLIOS", "1"); + qunsetenv("QET_EAGER_FOLIOS"); edit(edited, busiest); std::printf("edits on folio %d (%d links to other folios): %d folios not built\n", busiest + 1, most, edited.unloadedFolioCount()); @@ -482,9 +624,9 @@ int main(int argc, char **argv) //After everyday edits on a folio: as when built, others not built { QETProject edited(path), usual(path); - qunsetenv("QET_LAZY_FOLIOS"); + qputenv("QET_EAGER_FOLIOS", "1"); QETProject usual_edited(path); - qputenv("QET_LAZY_FOLIOS", "1"); + qunsetenv("QET_EAGER_FOLIOS"); edit(edited, busiest); edit(usual_edited, busiest); QFile file(path); file.open(QIODevice::ReadOnly); @@ -514,9 +656,9 @@ int main(int argc, char **argv) //links reach are built to be saved, the others not { QETProject renamed(path); - qunsetenv("QET_LAZY_FOLIOS"); + qputenv("QET_EAGER_FOLIOS", "1"); QETProject usual_renamed(path); - qputenv("QET_LAZY_FOLIOS", "1"); + qunsetenv("QET_EAGER_FOLIOS"); for (QETProject *project : {&renamed, &usual_renamed}) { Diagram *folio = project->folios().at(busiest); TitleBlockProperties block = folio->border_and_titleblock.exportTitleBlock(); @@ -531,9 +673,9 @@ int main(int argc, char **argv) //folio is built to be saved { QETProject changed(path); - qunsetenv("QET_LAZY_FOLIOS"); + qputenv("QET_EAGER_FOLIOS", "1"); QETProject usual_changed(path); - qputenv("QET_LAZY_FOLIOS", "1"); + qunsetenv("QET_EAGER_FOLIOS"); for (QETProject *project : {&changed, &usual_changed}) { DiagramContext properties = project->projectProperties(); properties.addValue(QStringLiteral("lazy_test"), QStringLiteral("1")); @@ -594,7 +736,7 @@ int main(int argc, char **argv) check(out.open(QIODevice::WriteOnly), "copy written"); out.write(document.toByteArray(4)); out.close(); - qputenv("QET_LAZY_FOLIOS", "1"); + qunsetenv("QET_EAGER_FOLIOS"); qputenv("QET_LAZY_FOLIOS_TRACE", "1"); static int built_late; built_late = 0; const QtMessageHandler previous = qInstallMessageHandler( @@ -607,6 +749,408 @@ int main(int argc, char **argv) check(opened.state() == QETProject::Ok, "a project with a shared uuid opens"); check(opened.unloadedFolioCount() == 0, "a project with a shared uuid opens with every folio built"); check(built_late == 0, "a project with a shared uuid builds its folios as it reads them, not later"); + } + //A .qetz opened lazily: no folio built but those + //the terminal strip needs, each built when shown, drawn and + //linked as when built, saved as the .qet is + { + const QString zipped = output + "/lazy_folios.qetz"; + QString error; + check(eager.writeContainerFile(zipped, &error), "saved as a .qetz"); + QETProject indexed(zipped); + check(indexed.state() == QETProject::Ok, "the .qetz opens"); + std::printf(".qetz opened: %d of %d folios not built (strip folios %d)\n", + indexed.unloadedFolioCount(), int(folios.size()), int(strip_folios.size())); + check(indexed.unloadedFolioCount() == folios.size() - strip_folios.size(), + "the .qetz opens building only the folios of the terminal strip's terminals"); + check(numbering(indexed) == numbering(eager), "symbols' numbering of a .qetz as when built"); + //The drawn items, through the view made of their tables: the + //folios built for it, as for the tables (D6: none were) + { + QETProject drawn(zipped); + auto count = [](QETProject &project) { + QSqlQuery query = project.dataBase()->newQuery(QStringLiteral("SELECT COUNT(*) FROM drawing_item_view")); + return query.exec() && query.next() ? query.value(0).toInt() : -1; + }; + const int lazily = count(drawn), built = count(eager); + std::printf(".qetz drawn items through their view: %d lazily, %d when built\n", lazily, built); + check(built > 0, "the project has drawn items"); + check(lazily == built, "the drawn items' view as when built"); + } + //Every wire's potential from the database as QElectroTech + //follows it with every folio built + { + QETProject indexed_potentials(zipped); + int same = 0, differ = 0, unknown = 0; + for (Diagram *folio : eager.folios()) + for (Conductor *wire : folio->conductors()) { + QSet live{wire->uuid()}; + for (Conductor *c : wire->relatedPotentialConductors(true)) live.insert(c->uuid()); + for (QETProject *project : {&eager, &indexed_potentials}) { + const auto stored = project->dataBase()->potentialWires(wire->uuid()); + if (!stored) { ++unknown; continue; } + if (*stored == live) { ++same; continue; } + if (differ++ < 3) + std::printf(" potential of %s: %d wires followed, %d from the database\n", + qPrintable(wire->uuid().toString()), int(live.size()), int(stored->size())); + } + } + std::printf("potentials from the database: %d same, %d differ, %d not answered\n", same, differ, unknown); + check(differ == 0 && same > 0, "every wire's potential from the database as followed"); + check(indexed_potentials.unloadedFolioCount() > 0, "potentials read without building"); + } + //The folios' rows as saved, not as reading their headers one + //by one numbers them ("1/1" for the first of three, D7) + { + auto folioRows = [](QETProject &project) { + QStringList rows; + for (const QString &row : tables(project)) + if (row.startsWith("diagram")) rows << row; + return rows; + }; + check(folioRows(indexed) == folioRows(eager), "the folios' rows of a .qetz as when built"); + //Folio numbers that count the folios ("%id/%total") + const QString totals = output + "/totals.qetz"; + qputenv("QET_EAGER_FOLIOS", "1"); + { + QETProject numbered(zipped); + QetScriptApi api(&numbered); + for (int i = 0 ; i < api.folioCount() ; ++i) + api.setFolioProperty(i, "folio", "%id/%total"); + check(numbered.writeContainerFile(totals, &error), "folio numbers with totals saved"); + } + QETProject usual_totals(totals); + qunsetenv("QET_EAGER_FOLIOS"); + QETProject lazy_totals(totals); + check(lazy_totals.unloadedFolioCount() > 0, "opened lazily"); + const QStringList lazy_rows = folioRows(lazy_totals), usual_rows = folioRows(usual_totals); + if (lazy_rows != usual_rows) + for (int i = 0 ; i < lazy_rows.size() && i < 3 ; ++i) + std::printf(" lazily %s\n built %s\n", qPrintable(lazy_rows.at(i).left(140)), + qPrintable(usual_rows.value(i).left(140))); + check(lazy_rows == usual_rows, "folio numbers with totals of a .qetz as when built"); + } + for (int i = 0; i < folios.size(); ++i) + same(indexed, i, "shown from the .qetz"); + check(indexed.toXml().toString() == eager.toXml().toString(), "the .qetz saves as when built"); + //A folio whose table continues one on another folio, shown + //first: the folios its table is built with (D5) + int continued = 0; + for (int i = 0; i < folios.size(); ++i) { + bool continues = false; + for (QGraphicsItem *item : folios.at(i)->items()) + if (item->type() == QetGraphicsTableItem::Type + && !static_cast(item)->previousTableUuid().isNull()) + continues = true; + if (!continues) continue; + QETProject fresh(zipped); + same(fresh, i, "a continued table shown first from the .qetz"); + ++continued; + } + check(continued > 0, "a folio whose table continues another"); + + //Numbering, the searches for symbols and the unused symbols + //are answered from the database (D5) + { + QETProject asked(zipped); + check(numbering(asked) == numbering(eager), "numbering of a .qetz as when built"); + for (const QString &title : eager.elementAutoNum().keys()) + check(gaps(asked, title) == gaps(eager, title), "a .qetz scheme's gaps as when built"); + auto locations = [](const QList &list) { + QStringList strings; + for (const ElementsLocation &location : list) strings << location.toString(); + strings.sort(); + return strings; + }; + check(locations(asked.unusedElements()) == locations(eager.unusedElements()), + "a .qetz's unused symbols as when built"); + const ElementProvider usual_search(&eager), search(&asked); + for (const ElementData::Types kinds : {ElementData::Types(ElementData::Slave), + ElementData::Types(ElementData::Master), + ElementData::Types(ElementData::AllReport)}) + check(found(search.freeElement(kinds)) == found(usual_search.freeElement(kinds)), + "free symbols of a .qetz found as when built"); + const int built = int(folios.size()) - asked.unloadedFolioCount(); + std::printf(".qetz asked numbering, searches, unused symbols: %d folios built\n", built); + check(asked.unloadedFolioCount() > 0, "asking a .qetz does not build every folio"); + } + + //A folio added, moved and removed: no other folio built, the + //folios' rows as when every folio is built (D5) + { + auto shuffle = [](QETProject &project) { + Diagram *added = project.addNewDiagram(); + project.diagramOrderChanged(1, 0); + project.removeDiagram(added); + }; + auto folioRows = [](QETProject &project) { + QStringList rows; + for (const QString &row : tables(project)) + if (row.startsWith("diagram")) rows << row; + return rows; + }; + QETProject shuffled(zipped); + const int unbuilt = shuffled.unloadedFolioCount(); + shuffle(shuffled); + std::printf(".qetz folio added, moved, removed: %d folios built\n", + unbuilt - shuffled.unloadedFolioCount()); + check(shuffled.unloadedFolioCount() == unbuilt, "adding, moving and removing a folio builds no other"); + qputenv("QET_EAGER_FOLIOS", "1"); + QETProject usual_shuffled(zipped); + qunsetenv("QET_EAGER_FOLIOS"); + shuffle(usual_shuffled); + check(folioRows(shuffled) == folioRows(usual_shuffled), "folios' rows after a move as when built"); + } + + //The tables a table can continue (its properties editor) and + //the wiring list's count of wires left out: only the folios + //holding a table built (D5) + { + QETProject listed(zipped); + const int unbuilt = listed.unloadedFolioCount(); + auto tableUuids = [](const QVector &list) { + QStringList uuids; + for (QetGraphicsTableItem *table : list) uuids << table->uuid().toString(); + uuids.sort(); + return uuids; + }; + const QStringList lazy_tables = tableUuids(ElementProvider(&listed).table(nullptr, nullptr)); + check(!lazy_tables.isEmpty(), "the project has tables"); + check(lazy_tables == tableUuids(ElementProvider(&eager).table(nullptr, nullptr)), + "the project's tables listed as when built"); + check(listed.dataBase()->excludedConductorCount() == eager.dataBase()->excludedConductorCount(), + "wires left out of the wiring list counted as when built"); + int with_tables = 0; + for (Diagram *folio : folios) { + for (QGraphicsItem *item : folio->items()) + if (item->type() == QetGraphicsTableItem::Type) { ++with_tables; break; } + } + std::printf(".qetz tables listed: %d folios built, %d hold a table\n", + unbuilt - listed.unloadedFolioCount(), with_tables); + check(listed.unloadedFolioCount() > 0, "listing the tables does not build every folio"); + } + + //A script (and the assistant's live mode) asking about the + //folios and one folio's items: that folio built, no other (D5) + { + QETProject scripted(zipped); + const int unbuilt = scripted.unloadedFolioCount(); + QetScriptApi api(&scripted), usual_api(&eager); + check(api.folioCount() == usual_api.folioCount(), "a script counts the folios as when built"); + for (int i = 0 ; i < folios.size() ; ++i) { + check(api.folioTitle(i) == usual_api.folioTitle(i) && api.folioUuid(i) == usual_api.folioUuid(i) + && api.folioBorder(i, "columns") == usual_api.folioBorder(i, "columns"), + "a script reads a folio's own data as when built"); + } + check(scripted.unloadedFolioCount() == unbuilt, "reading the folios' own data builds none"); + //The folio with the most links to other folios + const int target = busiest; + check(!scripted.folios().at(target)->isLoaded(), "a folio not built"); + QStringList lazy_uuids = api.elementUuids(target), usual_uuids = usual_api.elementUuids(target); + lazy_uuids.sort(); usual_uuids.sort(); + check(lazy_uuids == usual_uuids, "a script lists a folio's symbols as when built"); + QStringList lazy_wires = api.conductors(target), usual_wires = usual_api.conductors(target); + lazy_wires.sort(); usual_wires.sort(); + check(lazy_wires == usual_wires, "a script lists a folio's wires as when built"); + check(scripted.folios().at(target)->isLoaded(), "the folio a script reads is built"); + std::printf(".qetz script on folio %d: %d folios built\n", target + 1, + unbuilt - scripted.unloadedFolioCount()); + check(unbuilt - scripted.unloadedFolioCount() == 1, "a script reading one folio builds that folio only"); + } + + //A script changes the information of a symbol on a folio not + //built, which nothing else shows: through the project + //database, no folio built; saved as when built; undone (D5) + { + QETProject changed(zipped); + const int unbuilt = changed.unloadedFolioCount(); + int folio_index = -1; + QString uuid; + for (int i = 0 ; i < folios.size() && uuid.isEmpty() ; ++i) { + if (changed.folios().at(i)->isLoaded()) continue; + for (Element *element : folios.at(i)->elements()) + if (changed.dataBase()->canEditUnbuilt(element->uuid())) { + folio_index = i; uuid = element->uuid().toString(); break; + } + } + check(!uuid.isEmpty(), "a symbol to change through the database"); + QetScriptApi api(&changed); + const QString before = api.elementInfo(folio_index, uuid, "comment"); + const QString value = QStringLiteral("changed through the database"); + check(api.setElementInfo(folio_index, uuid, "comment", value), "a script changes a symbol not built"); + check(api.elementInfo(folio_index, uuid, "comment") == value, "the change read back"); + check(changed.unloadedFolioCount() == unbuilt, "changing a symbol not built builds no folio"); + const QString changed_path = output + "/changed.qetz"; + check(changed.writeContainerFile(changed_path, &error), "saved after the change"); + std::printf(".qetz symbol on folio %d changed by a script: %d folios built, %d by saving\n", + folio_index + 1, 0, unbuilt - changed.unloadedFolioCount()); + qputenv("QET_EAGER_FOLIOS", "1"); + const QString usual_path = output + "/changed-usual.qetz"; + { + QETProject usual_changed(zipped); + QetScriptApi usual_api(&usual_changed); + check(usual_api.setElementInfo(folio_index, uuid, "comment", value), "the same change when built"); + check(usual_changed.writeContainerFile(usual_path, &error), "saved after the change when built"); + } + QETProject changed_back(changed_path), usual_back(usual_path); + qunsetenv("QET_EAGER_FOLIOS"); + check(sortedLines(changed_back.toXml().toString(), path) == sortedLines(usual_back.toXml().toString(), path), + "a symbol changed through the database saved as when built"); + check(QetScriptApi(&changed_back).elementInfo(folio_index, uuid, "comment") == value, "the change saved"); + //Undone, then redone and the folio built: the symbol shows it + changed.undoStack()->undo(); + check(api.elementInfo(folio_index, uuid, "comment") == before, "the change undone"); + changed.undoStack()->redo(); + changed.buildFolioToShow(changed.folios().at(folio_index)); + Element *built_symbol = nullptr; + for (Element *element : changed.folios().at(folio_index)->elements()) + if (element->uuid().toString() == uuid) built_symbol = element; + check(built_symbol && built_symbol->elementInformations().value("comment").toString() == value, + "a symbol changed before its folio was built shows the change"); + } + + //A new revision on every folio: saved from the folios as read, + //not built; a label change builds its folio (D5) + revisionEveryFolio(zipped, output, "revised", path); + revisionEveryFolio(zipped, output, "relabelled", path, busiest); + + //A script changes a wire's cable by its uuid, on folios not + //built, the wire and its potential numbered by hand: through + //the project database, no folio built; saved as when built; + //undone; the folio built afterwards shows it (D5) + { + QETProject cabled(zipped); + const int unbuilt = cabled.unloadedFolioCount(); + int folio_index = -1; + QString uuid; + QSet potential; + for (int i = 0 ; i < folios.size() && uuid.isEmpty() ; ++i) { + if (cabled.folios().at(i)->isLoaded()) continue; + for (Conductor *wire : folios.at(i)->conductors()) { + QSet wires = wire->relatedPotentialConductors(true); + wires << wire; + bool usable = true; + for (Conductor *c : std::as_const(wires)) + if (!c->properties().m_formula.isEmpty() + || cabled.folios().at(eager.folioIndex(c->diagram()))->isLoaded()) + usable = false; + if (!usable || wires.size() < 2) continue; + folio_index = i; + uuid = wire->uuid().toString(); + for (Conductor *c : std::as_const(wires)) potential.insert(c->uuid()); + break; + } + } + check(!uuid.isEmpty(), "a wire numbered by hand, its potential on folios not built"); + QetScriptApi api(&cabled); + const QString before = api.conductorPropertyByUuid(folio_index, uuid, "cable"); + const QString value = QStringLiteral("W-42"); + check(api.setConductorPropertyByUuid(folio_index, uuid, "cable", value), "a script changes a wire not built"); + check(api.conductorPropertyByUuid(folio_index, uuid, "cable") == value, "the wire's change read back"); + for (const QUuid &wire : std::as_const(potential)) + check(cabled.dataBase()->conductorProperties(wire).m_cable == value, "every wire of the potential changed"); + check(cabled.unloadedFolioCount() == unbuilt, "changing wires not built builds no folio"); + const QString cabled_path = output + "/cabled.qetz"; + check(cabled.writeContainerFile(cabled_path, &error), "saved after the wire change"); + std::printf(".qetz wire on folio %d, potential of %d wires, cable changed by uuid: 0 folios built, %d by saving\n", + folio_index + 1, int(potential.size()), unbuilt - cabled.unloadedFolioCount()); + qputenv("QET_EAGER_FOLIOS", "1"); + const QString usual_path = output + "/cabled-usual.qetz"; + { + QETProject usual_cabled(zipped); + check(QetScriptApi(&usual_cabled).setConductorPropertyByUuid(folio_index, uuid, "cable", value), + "the same wire change when built"); + check(usual_cabled.writeContainerFile(usual_path, &error), "saved after the wire change when built"); + } + QETProject cabled_back(cabled_path), usual_back(usual_path); + qunsetenv("QET_EAGER_FOLIOS"); + auto lines = [&path](QETProject &project) { + QStringList list = sortedLines(project.toXml().toString(), path); + list.removeIf([](const QString &line) { return line.contains(QLatin1String("name=\"saved")); }); + return list; + }; + check(lines(cabled_back) == lines(usual_back), "a wire changed through the database saved as when built"); + cabled.undoStack()->undo(); + check(api.conductorPropertyByUuid(folio_index, uuid, "cable") == before, "the wire change undone"); + cabled.undoStack()->redo(); + Conductor *built_wire = nullptr; + for (Conductor *wire : cabled.folios().at(folio_index)->conductors()) + if (wire->uuid().toString() == uuid) built_wire = wire; + check(!built_wire, "the folio not built yet"); + cabled.buildFolioToShow(cabled.folios().at(folio_index)); + for (Conductor *wire : cabled.folios().at(folio_index)->conductors()) + if (wire->uuid().toString() == uuid) built_wire = wire; + check(built_wire && built_wire->properties().m_cable == value, + "a wire changed before its folio was built shows the change"); + } + + //The wire numbers and wiring list exports, and the terminal + //numbering dialog: as when built, building only the folios + //holding a terminal (D5) + { + QETProject exported(zipped); + const int unbuilt = exported.unloadedFolioCount(); + check(ConductorNumExport(&exported).wiresNum() == ConductorNumExport(&eager).wiresNum(), + "wire numbers exported as when built"); + const QString wiring = WiringListExport(&exported).toCsvString(), usual_wiring = WiringListExport(&eager).toCsvString(); + if (wiring != usual_wiring) { + const QStringList a = wiring.split('\n'), b = usual_wiring.split('\n'); + for (const QString &line : b) if (!a.contains(line)) std::printf(" wiring when built: %s\n", qPrintable(line)); + for (const QString &line : a) if (!b.contains(line)) std::printf(" wiring lazily: %s\n", qPrintable(line)); + } + check(wiring == usual_wiring, "wiring list exported as when built"); + check(exported.unloadedFolioCount() == unbuilt, "exporting wire numbers and the wiring list builds no folio"); + { TerminalNumberingDialog dialog(nullptr, &exported); } + int with_terminals = 0; + for (Diagram *folio : folios) + for (Element *element : folio->elements()) + if (element->elementData().m_type == ElementData::Terminal) { ++with_terminals; break; } + std::printf(".qetz terminal numbering: %d folios built, %d hold a terminal\n", + unbuilt - exported.unloadedFolioCount(), with_terminals); + check(exported.folios().size() - exported.unloadedFolioCount() <= with_terminals + (folios.size() - unbuilt), + "terminal numbering builds only the folios holding a terminal"); + } + + //Saved as a .qetz without building the folios not touched + //(D4): their parts and rows copied as they were + QETProject untouched(zipped); + const int unbuilt = untouched.unloadedFolioCount(); + const QString kept_path = output + "/kept.qetz"; + check(untouched.writeContainerFile(kept_path, &error), "saved as a .qetz without building"); + std::printf(".qetz saved untouched: %d of %d folios not built before, %d after\n", + unbuilt, int(folios.size()), untouched.unloadedFolioCount()); + check(untouched.unloadedFolioCount() > 0, "saving an untouched .qetz does not build every folio"); + qputenv("QET_EAGER_FOLIOS", "1"); + QETProject kept_back(kept_path); + qunsetenv("QET_EAGER_FOLIOS"); + check(kept_back.toXml().toString() == eager.toXml().toString(), + "a .qetz saved without building holds the project"); + + //After everyday edits on a folio + QETProject edited_z(zipped); + edit(edited_z, busiest); + const QString edited_path = output + "/edited.qetz"; + check(edited_z.writeContainerFile(edited_path, &error), "edited .qetz saved"); + std::printf(".qetz saved after the edits: %d folios not built\n", edited_z.unloadedFolioCount()); + check(edited_z.unloadedFolioCount() > 0, "saving an edited .qetz does not build every folio"); + //Against the same edits made with every folio built, saved and + //read back too: what a save and a reading do to a project is + //not what is checked here + const QString usual_path = output + "/edited-usual.qetz"; + qputenv("QET_EAGER_FOLIOS", "1"); + { + QETProject edited_usual(path); + edit(edited_usual, busiest); + check(edited_usual.writeContainerFile(usual_path, &error), "edits saved with every folio built"); + } + QETProject edited_back(edited_path), usual_back(usual_path); + qunsetenv("QET_EAGER_FOLIOS"); + check(sortedLines(edited_back.toXml().toString(), path) == sortedLines(usual_back.toXml().toString(), path), + "an edited .qetz saved without building holds what the edits made"); + //Each symbol's links as saved, partners on folios not built + //included, in their order (D5a) + check(savedLinks(edited_path, path) == savedLinks(usual_path, path), + "an edited .qetz saved without building keeps every symbol's links"); } std::printf("PASS: %d folios\n", int(folios.size())); } catch (const std::exception &error) { diff --git a/tests/qttest/run_lazy_folios_probe.py b/tests/qttest/run_lazy_folios_probe.py index 2c7f881cb..af41a0fcc 100644 --- a/tests/qttest/run_lazy_folios_probe.py +++ b/tests/qttest/run_lazy_folios_probe.py @@ -54,7 +54,7 @@ subprocess.run([ninja, '-C', str(build), '-f', probe_manifest.name, target], che 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) +result = subprocess.run([str(probe_binary), str(fixture), str(report), str(output)], timeout=1500) if report.exists(): print(report.read_text(encoding='utf-8', errors='replace')) sys.exit(result.returncode) diff --git a/tests/qttest/tst_qetcontainer.cpp b/tests/qttest/tst_qetcontainer.cpp index fa981f537..a8414759a 100644 --- a/tests/qttest/tst_qetcontainer.cpp +++ b/tests/qttest/tst_qetcontainer.cpp @@ -69,6 +69,19 @@ class tst_qetcontainer : public QObject return QetContainer::split(*original, QStringLiteral("tst_qetcontainer"), &error); } + //@p document without what QElectroTech computes, which a .qetz + //does not keep (format 4); tst_resaveunchanged checks that + //QElectroTech gives it back + static QDomDocument withoutComputed(const QDomDocument &document) + { + QDomDocument copy = document.cloneNode(true).toDocument(); + const bool own_uuids = QetContainer::ownUuids(copy.documentElement()); + for (QDomElement diagram = copy.documentElement().firstChildElement(QStringLiteral("diagram")); + !diagram.isNull() ; diagram = diagram.nextSiblingElement(QStringLiteral("diagram"))) + QetContainer::dropComputed(diagram, own_uuids); + return copy; + } + static QByteArray entry(const QList &entries, const QString &name) { for (const QetZip::Entry &e : entries) @@ -117,7 +130,7 @@ class tst_qetcontainer : public QObject QDomDocument back; QString error; if (!QetContainer::join(entries, &back, &error)) return error; - return difference(original.documentElement(), back.documentElement(), QString()); + return difference(withoutComputed(original).documentElement(), back.documentElement(), QString()); } private slots: @@ -151,7 +164,8 @@ private slots: QVERIFY2(QetZip::fromBytes(QetZip::toBytes(entries, &error), &unzipped, &error), qPrintable(error)); QDomDocument back; QVERIFY2(QetContainer::join(unzipped, &back, &error), qPrintable(error)); - const QString d = difference(original.documentElement(), back.documentElement(), QString()); + const QString d = difference(withoutComputed(original).documentElement(), + back.documentElement(), QString()); QVERIFY2(d.isEmpty(), qPrintable(d)); } @@ -187,7 +201,9 @@ private slots: // The engineering data is in project.sqlite and nowhere else: the folio // files hold no symbol information, links, wire numbers or title-block - // fields, and the database holds them all (industrial.qet: 50 folios). + // fields, nor what QElectroTech computes from them (a wire's + // description of its ends, a symbol text showing its information), and + // the database holds them all (industrial.qet: 50 folios). void databaseHoldsTheData() { QDomDocument original; @@ -232,6 +248,37 @@ private slots: sqlite3_close(db); } + // Nor does a folio file hold what QElectroTech computes (format 3): a + // wire's description of its ends, a symbol text showing its information. + // A file holding both, so that the check can fail. + void computedNotKept() + { + QDomDocument original; + QString error; + QVERIFY(QetContainer::parse(read(QStringLiteral(QET_EXAMPLES_DIR "/schema_unifilaire_voltaique2.qet")), + &original, &error)); + const QDomNodeList texts = original.elementsByTagName(QStringLiteral("dynamic_elmt_text")); + bool filled = false; + for (int i = 0 ; i < texts.size() && !filled ; ++i) { + QDomElement text = texts.at(i).toElement(); + if (text.attribute(QStringLiteral("text_from")) != QLatin1String("ElementInfo")) continue; + QDomElement content = text.firstChildElement(QStringLiteral("text")); + while (content.hasChildNodes()) content.removeChild(content.firstChild()); + content.appendChild(original.createTextNode(QStringLiteral("COMPUTED-LABEL"))); + filled = true; + } + QVERIFY(filled); + QVERIFY(original.toString().contains(QStringLiteral("element1_label="))); + const QList entries = QetContainer::split(original, QString(), &error); + QVERIFY2(!entries.isEmpty(), qPrintable(error)); + for (const QetZip::Entry &e : entries) { + if (!e.name.startsWith(QStringLiteral("folios/"))) continue; + QVERIFY2(!e.data.contains("element1_label=") && !e.data.contains("terminalname2="), + qPrintable(e.name)); + QVERIFY2(!e.data.contains("COMPUTED-LABEL"), qPrintable(e.name)); + } + } + // Terminal strips move into the database, and come back exactly: // industrial.qet with two strips on its own terminal symbols, parsed as // a save leaves it (no whitespace text). @@ -268,7 +315,8 @@ private slots: QVERIFY2(!e.data.contains(""), "the strips stayed in project.xml"); QDomDocument back; QVERIFY2(QetContainer::join(entries, &back, &error), qPrintable(error)); - const QString d = difference(original.documentElement(), back.documentElement(), QString()); + const QString d = difference(withoutComputed(original).documentElement(), + back.documentElement(), QString()); QVERIFY2(d.isEmpty(), qPrintable(d)); } @@ -307,7 +355,7 @@ private slots: QDomDocument back; QetContainer::Pictures pictures; QString error; - QVERIFY2(QetContainer::join(entries, &back, &error, &pictures), qPrintable(error)); + QVERIFY2(QetContainer::join(entries, &back, &error, nullptr, &pictures), qPrintable(error)); const QDomNodeList images = back.elementsByTagName(QStringLiteral("image")); const QDomNodeList original_images = original.elementsByTagName(QStringLiteral("image")); QCOMPARE(images.size(), 121); @@ -326,7 +374,7 @@ private slots: for (int i = 0 ; i < entries.size() ; ++i) if (entries.at(i).name.startsWith(QStringLiteral("images/"))) { entries.removeAt(i); break; } QDomDocument broken; - QVERIFY(!QetContainer::join(entries, &broken, &error, &pictures)); + QVERIFY(!QetContainer::join(entries, &broken, &error, nullptr, &pictures)); QVERIFY2(error.contains(QStringLiteral("missing")), qPrintable(error)); } @@ -348,9 +396,9 @@ private slots: QDomDocument back; QString error; int format = 0; - QVERIFY2(QetContainer::join(entries, &back, &error, nullptr, &format), qPrintable(error)); + QVERIFY2(QetContainer::join(entries, &back, &error, nullptr, nullptr, &format), qPrintable(error)); QCOMPARE(format, later); - const QString d = difference(original.documentElement(), back.documentElement(), QString()); + const QString d = difference(withoutComputed(original).documentElement(), back.documentElement(), QString()); QVERIFY2(d.isEmpty(), qPrintable(d)); } @@ -389,7 +437,7 @@ private slots: } // The schema in QETZ-FORMAT.md is the one written: same tables, same - // columns in the same order, same types and keys + // columns in the same order, same types; written without keys (format 4) void specMatchesSchema() { const QString spec = QString::fromUtf8(read(QStringLiteral(QET_FORMAT_SPEC))); @@ -402,15 +450,15 @@ private slots: const QList entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), &original); const QByteArray written = entry(entries, QStringLiteral("project.sqlite")); - auto describe = [](sqlite3 *db) { + auto describe = [](sqlite3 *db, const char *columns = "m.name, p.name, p.type") { QStringList out; sqlite3_stmt *st = nullptr; - sqlite3_prepare_v2(db, "SELECT m.name, p.name, p.type, p.pk, p.\"notnull\" FROM sqlite_master m, " + sqlite3_prepare_v2(db, QByteArray("SELECT ") + columns + " FROM sqlite_master m, " "pragma_table_info(m.name) p WHERE m.type = 'table' ORDER BY m.name, p.cid", -1, &st, nullptr); while (st && sqlite3_step(st) == SQLITE_ROW) { QStringList row; - for (int i = 0 ; i < 5 ; ++i) + for (int i = 0 ; i < sqlite3_column_count(st) ; ++i) row << QString::fromUtf8(reinterpret_cast(sqlite3_column_text(st, i))); out << row.join(QLatin1Char(' ')); } @@ -426,6 +474,9 @@ private slots: memcpy(copy, written.constData(), size_t(written.size())); sqlite3_deserialize(from_file, "main", copy, written.size(), written.size(), SQLITE_DESERIALIZE_FREEONCLOSE); const QStringList spec_tables = describe(from_spec), file_tables = describe(from_file); + const QStringList keys = describe(from_file, "p.pk + p.\"notnull\""); + QVERIFY(!keys.isEmpty()); + QCOMPARE(keys.count(QStringLiteral("0")), keys.size()); sqlite3_close(from_spec); sqlite3_close(from_file); QVERIFY(!spec_tables.isEmpty()); @@ -461,6 +512,25 @@ private slots: // A crafted project.sqlite whose "element" is a view, not a table, is // refused: nothing in a file's schema is trusted. + void garbageDatabaseRefused() + { + //Bytes that are no database: refused, not read (crashed reading + //the first statement until stage D7) + QDomDocument original; + QList entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"), &original); + bool replaced = false; + for (QetZip::Entry &entry : entries) + if (entry.name == QLatin1String("project.sqlite")) { + entry.data = QByteArray("not a database").repeated(100); + replaced = true; + } + QVERIFY(replaced); + QDomDocument back; + QString error; + QVERIFY(!QetContainer::join(entries, &back, &error)); + QVERIFY(!error.isEmpty()); + } + // project.sqlite changed by @p sql in @p entries static void alterDatabase(QList &entries, const char *sql) { @@ -492,6 +562,9 @@ private slots: << QByteArray("INSERT INTO element SELECT 'x', folio_uuid, prefix, freezeLabel, info_attrs," " links_attrs FROM element LIMIT 1") << QStringLiteral("symbol rows"); + QTest::newRow("symbol row twice") + << QByteArray("INSERT INTO element SELECT * FROM element LIMIT 1") + << QStringLiteral("symbol rows"); QTest::newRow("wire row of no wire") << QByteArray("INSERT INTO conductor SELECT 'x', folio_uuid, element1, terminal1, element2, terminal2," " num, formula, function, tension_protocol, conductor_color, conductor_section, cable," diff --git a/tests/qttest/tst_resaveunchanged.cpp b/tests/qttest/tst_resaveunchanged.cpp index c36ac8127..72a8186a7 100644 --- a/tests/qttest/tst_resaveunchanged.cpp +++ b/tests/qttest/tst_resaveunchanged.cpp @@ -8,6 +8,10 @@ #include #include +#include "container/qetzip.h" + +#include + // Saving a project that was just saved must change nothing. Two things // made the second save differ from the first, both cleanup done on save // but not on load: @@ -306,6 +310,10 @@ private slots: QVERIFY2(!zipped.isEmpty(), "--resave as .qetz failed"); QVERIFY2(read(zipped).startsWith("PK\x03\x04"), "the .qetz is not a zip"); QVERIFY2(read(zipped).size() < read(straight).size() / 2, "the .qetz is not smaller"); + QCOMPARE(databaseTables(zipped), QStringList({ + "conductor", "element", "element_info", "folio", "folio_property", "link", "meta", + "project_property", "strip_bridge", "strip_bridge_terminal", "strip_terminal", + "terminal_strip"})); const QString back = resave(zipped); QVERIFY2(!back.isEmpty(), "opening the .qetz failed"); @@ -317,6 +325,31 @@ private slots: QCOMPARE(without_save_stamps(read(back)), without_save_stamps(read(straight))); } + // The tables of the database in the .qetz @p path: the document's + // engineering data only, each fact once -- not QElectroTech's own + // project database, built from it when the project opens + static QStringList databaseTables(const QString &path) + { + QList entries; + if (!QetZip::fromBytes(read(path), &entries)) return {QStringLiteral("not a zip")}; + QByteArray database; + for (const QetZip::Entry &entry : std::as_const(entries)) + if (entry.name == QLatin1String("project.sqlite")) database = entry.data; + sqlite3 *db = nullptr; + sqlite3_open(":memory:", &db); + unsigned char *copy = static_cast(sqlite3_malloc64(sqlite3_uint64(database.size()))); + memcpy(copy, database.constData(), size_t(database.size())); + sqlite3_deserialize(db, "main", copy, database.size(), database.size(), SQLITE_DESERIALIZE_FREEONCLOSE); + sqlite3_stmt *list = nullptr; + QStringList tables; + sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name", -1, &list, nullptr); + while (list && sqlite3_step(list) == SQLITE_ROW) + tables << QString::fromUtf8(reinterpret_cast(sqlite3_column_text(list, 0))); + sqlite3_finalize(list); + sqlite3_close(db); + return tables; + } + // 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) @@ -328,7 +361,7 @@ private slots: 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("QET_EAGER_FOLIOS"), QStringLiteral("1")); //every folio built on opening } env.insert(QStringLiteral("HOME"), home); env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config")); @@ -479,6 +512,40 @@ private slots: QCOMPARE(rowsOf(edited), rowsOf(reopened)); } + // A project read from a .qet and saved where it is, is saved as a .qetz + // beside it, and the .qet is left as it was (stage D1). + // What the .qetz holds is what a plain save of the .qet holds. + void qetSavedAsQetzBeside() + { +#ifndef QET_HAS_SCRIPTING + QSKIP("needs --run: this QElectroTech is built without Qt Qml"); +#endif + const QString qet = m_dir.filePath(QStringLiteral("beside.qet")); + QFile::remove(qet); + QVERIFY(QFile::copy(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), qet)); + const QByteArray before = read(qet); + const QString script = m_dir.filePath(QStringLiteral("beside.js")); + QFile js(script); + QVERIFY(js.open(QIODevice::WriteOnly)); + js.write("qet.log('SAVED ' + qet.save(''));\n"); + js.close(); + const QString log = runChecked({QStringLiteral("--run"), script, qet}); + QVERIFY2(log.contains(QStringLiteral("SAVED true")), qPrintable(log.right(400))); + + QCOMPARE(read(qet), before); + const QString qetz = m_dir.filePath(QStringLiteral("beside.qetz")); + QVERIFY2(read(qetz).startsWith("PK\x03\x04"), "no .qetz beside the .qet"); + + auto without_save_stamps = [](QByteArray xml) { + static const QRegularExpression stamp(QStringLiteral( + "\\s*]*name=\"saved(filename|filepath|filenamedir|date|date-us|date-eu|time)\"[^>]*>[^<]*")); + return QString::fromUtf8(xml).remove(stamp); + }; + const QString straight = resave(qet); + const QString back = resave(qetz); + QVERIFY(!straight.isEmpty() && !back.isEmpty()); + QCOMPARE(without_save_stamps(read(back)), without_save_stamps(read(straight))); + } // 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.