Merge pull request #1472 from ispyisail/feat/qetz-db-truth-v2

Save as .qetz by default, with the project database as the source of truth
This commit is contained in:
Laurent Trinques
2026-10-14 17:22:44 +02:00
committed by GitHub
51 changed files with 3545 additions and 620 deletions
+4
View File
@@ -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"
+48
View File
@@ -10898,6 +10898,10 @@ Donnez-leur une autre numérotation d&apos;abord.</numerusform>
<source>Management</source>
<translation>Management</translation>
</message>
<message>
<source>Project QElectroTech (*.qet *.qetz)</source>
<translation>Projets QElectroTech (*.qet *.qetz)</translation>
</message>
</context>
<context>
<name>ProjectDBModel</name>
@@ -11409,6 +11413,33 @@ Voulez-vous enregistrer les modifications ?</translation>
<comment>window title for a project-less ProjectView</comment>
<translation>Projet</translation>
</message>
<message>
<source>Zipped project QElectroTech (*.qetz)</source>
<comment>filetypes allowed when saving a project file</comment>
<translation>Projet QElectroTech compressé (*.qetz)</translation>
</message>
<message>
<source>Save a copy for an older QElectroTech</source>
<comment>dialog title</comment>
<translation>Enregistrer une copie pour une version antérieure de QElectroTech</translation>
</message>
<message>
<source>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 &gt; Save a copy for an older QElectroTech.</source>
<translation>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 &gt; Enregistrer une copie pour une version antérieure de QElectroTech.</translation>
</message>
<message>
<source>Save as a zipped project</source>
<comment>message box title</comment>
<translation>Enregistrement au format de projet compressé</translation>
</message>
<message>
<source>%1 already exists. Replace it?</source>
<translation>%1 existe déjà. Le remplacer ?</translation>
</message>
</context>
<context>
<name>PropertiesEditorDockWidget</name>
@@ -13692,6 +13723,19 @@ Déponter et/ou supprimer les niveaux des bornes concerné afin de pouvoir les s
<comment>message box title</comment>
<translation>Erreur</translation>
</message>
<message>
<source>Save a copy for an older QElectroTech</source>
<translation>Enregistrer une copie pour une version antérieure de QElectroTech</translation>
</message>
<message>
<source>Writes a copy of the current project as a .qet file, which versions of QElectroTech that cannot open a .qetz open</source>
<comment>status bar tip</comment>
<translation>Enregistre une copie du projet courant au format .qet, que savent ouvrir les versions de QElectroTech qui ne peuvent pas ouvrir un .qetz</translation>
</message>
<message>
<source>QElectroTech project(*.qet *.qetz);;XML Files (*.xml);;All files (*)</source>
<translation>Projets QElectroTech (*.qet *.qetz);;Fichiers XML (*.xml);;Tous les fichiers (*)</translation>
</message>
</context>
<context>
<name>QETElementEditor</name>
@@ -17242,6 +17286,10 @@ Pour l&apos;activer : Configurer QElectroTech &gt; Général &gt; Projets, ou d
<source>Ungroup a set of terminals</source>
<translation>Dégrouper un ensemble de bornes</translation>
</message>
<message>
<source>Change the information of the element</source>
<translation>Modifier les informations de l&apos;élément</translation>
</message>
</context>
<context>
<name>QTextOrientationWidget</name>
+1
View File
@@ -32,6 +32,7 @@
<key>CFBundleTypeExtensions</key>
<array>
<string>qet</string>
<string>qetz</string>
</array>
<key>CFBundleTypeRole</key>
<string>Editor</string>
+1 -1
View File
@@ -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.
+8
View File
@@ -34,6 +34,14 @@
<comment xml:lang="zh">QElectroTech 项目文件</comment>
<glob pattern="*.qet" />
</mime-type>
<mime-type type="application/x-qelectrotech-project+zip">
<sub-class-of type="application/zip"/>
<comment>QElectroTech zipped project file</comment>
<glob pattern="*.qetz" />
<magic priority="60">
<match type="string" offset="30" value="mimetypeapplication/x-qelectrotech-project+zip"/>
</magic>
</mime-type>
<mime-type type="application/x-qet-element">
<sub-class-of type="text/xml"/>
<comment>QElectroTech element file</comment>
+7
View File
@@ -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 |
+55 -7
View File
@@ -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 <elementInformations> 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 <tag> carry a
"""Sweep every .qet and .qetz in a directory, counting how many <tag> 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 "
+89
View File
@@ -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)'}")
@@ -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);
+4 -3
View File
@@ -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<QUuid> 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
+5 -7
View File
@@ -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<int, QByteArray> 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<int, QByteArray> kept_folios;
const QDomDocument doc = project.toXml(&kept_folios);
QFile file(output);
+25 -4
View File
@@ -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 <QFileDialog>
@@ -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<QUuid> 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)) {
+23 -6
View File
@@ -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` | `<qetz format="3" min-reader="3" writer="QElectroTech …"/>` |
| `manifest.xml` | `<qetz format="4" min-reader="4" writer="QElectroTech …"/>` |
| `project.xml` | the project document, each part replaced by `<qetz-part file="…"/>` |
| `project.sqlite` | the engineering data (section 4) |
| `folios/<uuid>.xml` | one `<diagram>` 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).
<!-- schema: tst_qetcontainer checks it against the database written -->
```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.
+152 -7
View File
@@ -22,6 +22,8 @@
#include <QHash>
#include <QMap>
#include <QSet>
#include <QRegularExpression>
#include <QUuid>
const char QetContainer::MimeType[] = "application/x-qelectrotech-project+zip";
@@ -93,6 +95,20 @@ QList<QDomElement> 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/<categories>/<name>
void splitCollection(QDomDocument &document, const QDomElement &category, const QString &path,
QList<QetZip::Entry> *parts, QSet<QString> *taken)
@@ -164,7 +180,7 @@ bool QetContainer::readDocument(const QByteArray &content, QDomDocument *documen
return parse(content, document, error);
QList<QetZip::Entry> 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<QString> 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<QetZip::Entry> QetContainer::split(const QDomDocument &project, const QString &writer,
QString *error)
QString *error, const QHash<QString, QByteArray> &kept_from)
{
QDomDocument document = project.cloneNode(true).toDocument();
QDomElement root = document.documentElement();
@@ -190,7 +276,34 @@ QList<QetZip::Entry> 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 (<diagram qetz-kept-part="..."/>,
//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<QDomElement> 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<QString> 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<QetZip::Entry> 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<QString, QByteArray> 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<QetZip::Entry> 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<QetZip::Entry> &entries, QDomDocument *project, QString *error,
Pictures *pictures, int *format)
QStringList *folio_files, Pictures *pictures, int *format)
{
QHash<QString, QByteArray> files;
for (const QetZip::Entry &entry : entries)
@@ -312,6 +454,9 @@ bool QetContainer::join(const QList<QetZip::Entry> &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;
+18 -9
View File
@@ -57,30 +57,39 @@ namespace QetContainer
using Pictures = QHash<QString, QByteArray>;
/// 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<QetZip::Entry> split(const QDomDocument &project,
const QString &writer = QString(),
QString *error = nullptr);
QString *error = nullptr,
const QHash<QString, QByteArray> &kept_from = QHash<QString, QByteArray>());
bool join(const QList<QetZip::Entry> &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
+480 -232
View File
@@ -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<QDomElement> &items, QList<bool> *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<QDomElement> &items, QList<bool> *own,
const QSet<QString> &reserved = QSet<QString>())
{
static const QUuid key_namespace(QStringLiteral("{3f0c6c1e-5b8e-4b8a-9d55-6b2f5f0f9a41}"));
QHash<QString, int> seen;
for (const QDomElement &e : items)
if (!QUuid(e.attribute(QStringLiteral("uuid"))).isNull()) ++seen[e.attribute(QStringLiteral("uuid"))];
QSet<QString> taken(seen.keyBegin(), seen.keyEnd());
taken.unite(reserved);
QHash<QString, int> 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<QDomElement> &items, QList<bool> *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<const char *>(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<const char *>(sqlite3_column_text(info, 0))),
QString::fromUtf8(reinterpret_cast<const char *>(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<unsigned char *>(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<QString> folios, elements, conductors;
bool use(QSet<QString> &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<QDomElement> &items, QList<bool> *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<unsigned char *>(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 <elementInformation> 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<QString> 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<bool> 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<unsigned char *>(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<QString, QVariant> 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<QVariantList> *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<QString> folios_used, elements_used, conductors_used;
auto use = [error](QSet<QString> &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<std::pair<QString, int>> 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;
}
+4 -1
View File
@@ -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
+514 -14
View File
@@ -49,6 +49,8 @@
#include <QMetaEnum>
#include <QTextDocument>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRegularExpression>
#include <QSqlDriver>
#include <QSqlError>
@@ -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<QUuid, DocumentTerminal> 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<Diagram *> 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<QUuid> 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<QUuid, QString> projectDataBase::elementTypesOnFolios(const QSet<QUuid> &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::FolioSymbol> projectDataBase::symbolsOnFolios(const QList<QUuid> &folios) const
{
QList<FolioSymbol> 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::FolioWire> projectDataBase::wiresOnFolios(const QList<QUuid> &folios) const
{
QList<FolioWire> 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<QSet<QUuid>> projectDataBase::potentialWires(const QUuid &wire) const
{
using Node = std::pair<QString, QString>; //element uuid, terminal uuid
QHash<QString, std::pair<Node, Node>> ends;
QHash<Node, QStringList> wires_at;
QHash<QString, QList<Node>> 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<QString, ElementData::Type> 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<QUuid> wires;
QSet<Node> seen;
QList<Node> 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<QUuid> 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<QUuid> projectDataBase::linkedSymbols(const QUuid &element) const
{
QList<QUuid> 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<DiagramContext> 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<QUuid> projectDataBase::foliosWithUnbuiltEdits() const
{
QSet<QUuid> 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<ConductorProperties> 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<QUuid, ConductorProperties> projectDataBase::unbuiltWireEditsOnFolio(const QUuid &folio) const
{
QHash<QUuid, ConductorProperties> 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<QPointer<Conductor>> 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<QUuid, DiagramContext> projectDataBase::unbuiltEditsOnFolio(const QUuid &folio, bool *need_build) const
{
QHash<QUuid, DiagramContext> 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<QPointer<Element>> 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<QUuid> projectDataBase::folioUuidsOfElements(const QSet<QUuid> &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()));
}
/**
+43 -2
View File
@@ -29,9 +29,12 @@
#include <QSet>
#include <QUuid>
#include <optional>
#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<QUuid> foliosWithUnjoinedWires() const;
QSet<QUuid> folioUuidsOfElements(const QSet<QUuid> &elements) const;
bool canEditUnbuilt(const QUuid &element) const;
void editUnbuiltElementInformation(const QUuid &element, DiagramContext information);
std::optional<DiagramContext> takeUnbuiltEdit(const QUuid &element);
QSet<QUuid> foliosWithUnbuiltEdits() const;
QHash<QUuid, DiagramContext> 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<ConductorProperties> takeUnbuiltWireEdit(const QUuid &wire);
QHash<QUuid, ConductorProperties> unbuiltWireEditsOnFolio(const QUuid &folio) const;
bool setPlacedConductorProperties(const QUuid &wire, const ConductorProperties &properties);
QHash<QUuid, QString> elementTypesOnFolios(const QSet<QUuid> &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<FolioSymbol> symbolsOnFolios(const QList<QUuid> &folios) const;
QList<QUuid> 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<FolioWire> wiresOnFolios(const QList<QUuid> &folios) const;
QString elementLabel(const QUuid &element) const;
QString terminalName(const QUuid &terminal, const QUuid &element) const;
std::optional<QSet<QUuid>> 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<QUuid, QPointer<Element>> 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<QUuid, UnbuiltEdit> m_unbuilt_edits;
/// The same for wires (editUnbuiltConductorProperties())
struct UnbuiltWireEdit { ConductorProperties properties; QUuid folio; };
QHash<QUuid, UnbuiltWireEdit> m_unbuilt_wire_edits;
QHash<QUuid, TitleBlockProperties> m_folio_titleblocks;
QHash<QUuid, QPointer<Diagram>> m_placed_folios;
QHash<QUuid, ConductorProperties> m_conductor_properties;
@@ -314,7 +355,7 @@ class projectDataBase : public QObject
//The folios whose rows a rebuild keeps (keepUnbuiltFolioRows())
QSet<Diagram *> 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);
+181 -59
View File
@@ -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<Element *> Diagram::buildSymbols(const QSet<QUuid> &uuids)
{
QList<Element *> 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<QString> 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 <Conductor *> > 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<QString,
}
}
/**
@brief Diagram::buildElement
Make the symbol @p element_xml describes and add it to this folio, as
fromXml() does for each symbol it reads.
@return the symbol, or nullptr if it could not be made (said in the log)
*/
Element *Diagram::buildElement(QDomElement element_xml, QHash<int, Terminal *> &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<Element *> added_elements;
QHash<int, Terminal *> 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
+20 -1
View File
@@ -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<QUuid, Element *> m_partial_elements;
QHash<int, Terminal *> m_partial_table;
bool m_partial_changed = false;
Element *buildElement(QDomElement element_xml, QHash<int, Terminal *> &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<Element *> buildSymbols(const QSet<QUuid> &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<QUuid, Element *> 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
+29 -10
View File
@@ -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<DiagramImageItem *>(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<Conductor *> potentials = potentialConductors();
for (Conductor *c : other.potentialConductors())
{
QList<Conductor *> 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<Conductor *> conductors)
{
const QList<Conductor *> 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<Conductor *> DiagramContent::potentialConductors() const
{
if (!m_potential_conductors) {
QList<Conductor *> 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
+8 -1
View File
@@ -19,6 +19,7 @@
#define DIAGRAM_CONTENT_H
#include <QSet>
#include <optional>
#include <QVector>
#include "../qetgraphicsitem/terminalelement.h"
@@ -77,7 +78,6 @@ class DiagramContent
QList<Conductor *> m_conductors_to_update;
QList<Conductor *> m_conductors_to_move;
QList<Conductor *> m_other_conductors;
QList<Conductor *> m_potential_conductors;
QSet<DynamicElementTextItem *> m_element_texts;
QSet<ElementTextItemGroup *> m_texts_groups;
QList<QGraphicsItem *> m_selected_items;
@@ -89,6 +89,7 @@ class DiagramContent
QList<DiagramTextItem *> selectedTexts() const;
QList<ElementTextItemGroup *> selectedTextsGroup() const;
QList<Conductor *> conductors(int = AnyConductor) const;
QList<Conductor *> potentialConductors() const;
bool hasDeletableItems() const;
bool hasCopiableItems() const;
QList<QGraphicsItem *> items(int = All) const;
@@ -103,6 +104,12 @@ class DiagramContent
private:
int removePinnedGroups();
/// The wires potentialConductors() is made from, in order
QList<Conductor *> 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<QList<Conductor *>> m_potential_conductors;
};
QDebug &operator<<(QDebug, DiagramContent &);
#endif
+1 -1
View File
@@ -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()
{
+10 -3
View File
@@ -55,7 +55,7 @@ ElementProvider::ElementProvider(const QList<Diagram *> &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<Diagram *> ElementProvider::folios(ElementData::Types kinds, bool free_only) const
@@ -98,7 +98,7 @@ QVector <QPointer<Element>> ElementProvider::freeElement(ElementData::Types filt
QList<Element *> elmt_list;
const QList<Diagram *> 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<QUuid> waiting = m_project ? m_project->symbolsOnUnbuiltFolios()
: QSet<QUuid>();
@@ -199,7 +199,14 @@ QVector<QetGraphicsTableItem *> ElementProvider::table(
}
}
for (auto d : allFolios()) {
//Only the folios holding a table are built for it
QList<Diagram *> 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)
+1 -1
View File
@@ -55,7 +55,7 @@ class ElementProvider
QList <Diagram *> 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;
+56 -3
View File
@@ -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<DiagramView *>(m_tab->widget(tab_id)))
+1
View File
@@ -131,6 +131,7 @@ class ProjectView : public QWidget
QETResult save();
QETResult saveAs();
QETResult doSave();
QETResult saveCopyForOlderVersions();
int cleanProject();
void updateWindowTitle();
void updateTabTitle(DiagramView *);
+16
View File
@@ -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);
+1
View File
@@ -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
@@ -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) {
@@ -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
{
+5
View File
@@ -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;
+41 -11
View File
@@ -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<std::pair<QUuid, int>> links;
if (keepsSavedLinks() && diagram() && diagram()->project()) {
const QSet<QUuid> waiting = diagram()->project()->waitingLinkTargets();
QSet<Element *> 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<QUuid> 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<QUuid> Element::pendingLinks() const
+9 -1
View File
@@ -1099,7 +1099,7 @@ QList<Terminal *> 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<Terminal *> relatedPotentialTerminal (
project->buildFoliosHoldingSymbols(waiting);
}
QList <Element *> 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());
+377 -95
View File
@@ -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<QetZip::Entry> 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<Diagram *> 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<Diagram *> QETProject::folios() const
{
@@ -603,9 +628,9 @@ QList<Diagram *> 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<Diagram *> QETProject::cableLabelFolios()
{
@@ -629,7 +654,7 @@ QList<Diagram *> 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<Diagram *> QETProject::builtFolios() const
{
@@ -722,6 +747,21 @@ void QETProject::buildFolios(const QList<Diagram *> &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<Diagram *> &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<Diagram *> 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<QUuid> &symbols)
return;
}
QList<Diagram *> to_build;
//Filled from the document, the database knows where every symbol is
if (m_data_base.rowsFromDocument()) {
const QSet<QUuid> 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<QUuid> &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<QUuid> 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<Diagram *> &folios, const QSet<QUuid> &partners)
{
const ProjectState state = m_state;
m_state = ProjectParsingRunning;
QList<Element *> built;
for (Diagram *d : folios) {
const QList<Element *> 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<QUuid> 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> QETProject::symbolNumbering() const
{
@@ -1846,38 +1923,27 @@ QList<QETProject::SymbolNumbering> QETProject::symbolNumbering() const
return list;
}
//A symbol the database has no row for is one building leaves out
//(its definition is missing)
const QHash<QUuid, QString> types = m_data_base.elementTypesOnFolios(unbuilt);
const QHash<QString, QStringList> 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<QUuid> unbuilt_in_order;
for (Diagram *d : m_diagrams_list)
if (!d->isLoaded()) unbuilt_in_order << d->uuid();
const QHash<QString, QStringList> 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<int, QByteArray> *kept_folios)
QDomDocument QETProject::toXml(QHash<int, QByteArray> *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 <diagram qetz-kept-part="..."/>, 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 <diagram qet_kept_folio="n"/> 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<int, QByteArray> *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<QUuid> edited = m_data_base.foliosWithUnbuiltEdits();
QList<Diagram *> 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<QUuid> waiting;
QSet<Diagram *> 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<int, QByteArray> *kept_folios)
} else {
loadFolios();
}
m_waiting_link_targets = unloadedFolioCount() ? symbolsOnUnbuiltFolios() : QSet<QUuid>();
//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<int, QByteArray> *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<QUuid, DiagramContext> 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<QUuid, ConductorProperties> 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<QUuid, DiagramContext> 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<QUuid, ConductorProperties> 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<int, QByteArray> *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<QString, QByteArray> &kept_from)
{
const QList<QetZip::Entry> entries = QetContainer::split(
QList<QetZip::Entry> 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<QString, QByteArray>() : 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<int, QByteArray> 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<int, QByteArray> 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<QUuid>();
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<Diagram *> 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<ElementsLocation> QETProject::unbuiltFolioElements() const
{
QSet<QString> 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<QUuid> 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<ElementsLocation> 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());
+38 -4
View File
@@ -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 <QFuture>
#include <array>
#include <memory>
#include <optional>
class Diagram;
@@ -123,7 +125,7 @@ class QETProject : public QObject
ProjectState state() const;
QList<Diagram *> diagrams() const;
//The folios whether their items are built or not
//(QET_LAZY_FOLIOS); diagrams() builds them
//(lazy folios); diagrams() builds them
QList<Diagram *> folios() const;
bool deferringFolios() const {return m_deferring_folios;}
int unloadedFolioCount() const;
@@ -131,9 +133,15 @@ class QETProject : public QObject
QList<Diagram *> cableLabelFolios();
QList<Diagram *> foliosWithWiresNotReconnected();
void buildFolioToShow(Diagram *folio);
void buildPartnerSymbols(const QList<Diagram *> &folios, const QSet<QUuid> &partners);
void buildFoliosHolding(ElementData::Types kinds, bool free_only);
void buildFoliosHoldingSymbols(const QSet<QUuid> &symbols);
void buildFoliosHoldingTables();
QSet<QUuid> symbolsOnUnbuiltFolios() const;
/// While saving: the symbols on folios not built a built symbol
/// may be saved linked to (Element::toXml())
QSet<QUuid> waitingLinkTargets() const {return m_waiting_link_targets;}
QDomDocument folioXml(Diagram *folio);
QSet<Diagram *> tableChainFolios(Diagram *folio) const;
void loadFolios() const;
void buildFolios(const QList<Diagram *> &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<Element *> 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<int, QByteArray> *kept_folios = nullptr);
QDomDocument toXml(QHash<int, QByteArray> *kept_folios = nullptr,
QStringList *kept_parts = nullptr);
static QString spliceKeptFolios(QString text, const QHash<int, QByteArray> &kept_folios);
bool close();
QETResult write();
@@ -328,6 +343,7 @@ class QETProject : public QObject
bool usesElement(const ElementsLocation &) const;
QList <ElementsLocation> unusedElements() const;
QList<ElementsLocation> 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<QString, QByteArray> &kept_from = QHash<QString, QByteArray>());
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<QUuid> m_waiting_link_targets;
/// The entries of the .qetz opened, and the part each folio
/// was read from, in order
QHash<QString, QByteArray> 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
+3 -3
View File
@@ -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()
+318 -82
View File
@@ -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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<DiagramContext> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Conductor *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Conductor *> 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<QString, QString> 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<QString> 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<QUuid, std::pair<ConductorProperties, ConductorProperties>> &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<QETProject> m_project;
QHash<QUuid, std::pair<ConductorProperties, ConductorProperties>> 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<QUuid, std::pair<ConductorProperties, ConductorProperties>> 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<QetScriptApi *>(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<T *> sortedByPosition(const QSet<T *> &items)
QList<IndependentTextItem *> QetScriptApi::sortedTexts(int folioIndex) const
{
if (!m_project) return {};
const QList<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<QetShapeItem *> QetScriptApi::sortedShapes(int folioIndex) const
{
if (!m_project) return {};
const QList<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<QetGraphicsTableItem *> QetScriptApi::sortedTables(int folioIndex) const
{
if (!m_project) return {};
const QList<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<QetGraphicsTableItem *> before = sortedTables(folioIndex);
QetGraphicsTableFactory::create(diagrams.at(folioIndex), &dialog);
QetGraphicsTableFactory::create(built(diagrams.at(folioIndex)), &dialog);
const QList<QetGraphicsTableItem *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<QetGraphicsTableItem *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<DiagramImageItem *> QetScriptApi::sortedImages(int folioIndex) const
{
if (!m_project) return {};
const QList<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Element *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> folios;
const QSet<QUuid> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> 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<Diagram *> diagrams = m_project->diagrams();
const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return;
diagrams.at(folioIndex)->clearSelection();
}
+17
View File
@@ -24,11 +24,17 @@
#include <QVariantList>
#include <QVariantMap>
#include <optional>
#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<DiagramContext> 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);
+12 -2
View File
@@ -13,6 +13,16 @@
#include <QSet>
#include <algorithm>
namespace {
//The folios holding a terminal, built: a folio not built yet
//(lazy folios) holding none is not built for this
QList<Diagram *> 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<QString> prefixes;
foreach (Diagram *diagram, project->diagrams()) {
foreach (Diagram *diagram, terminalFolios(project)) {
foreach (QGraphicsItem *qgi, diagram->items()) {
if (Element *elmt = qgraphicsitem_cast<Element *>(qgi)) {
if (elmt->elementData().m_type == ElementData::Terminal) {
@@ -153,7 +163,7 @@ QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const
QList<TermInfo> 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<Element *>(qgi)) {
@@ -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);
}
@@ -21,8 +21,11 @@
#include "../diagramcontext.h"
#include <QUndoCommand>
#include <QUuid>
#include <QPointer>
class Element;
class QETProject;
/**
@brief The ChangeElementInformationCommand class
@@ -52,4 +55,26 @@ class ChangeElementInformationCommand : public QUndoCommand
QMap<QPointer<Element>, QPair<DiagramContext, DiagramContext>> 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<QETProject> m_project;
QUuid m_element;
DiagramContext m_old_info, m_new_info;
};
#endif // CHANGEELEMENTINFORMATIONCOMMAND_H
+89 -3
View File
@@ -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 <QFileDialog>
#include <QMessageBox>
#include <QTextStream>
@@ -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<QUuid, Element *> built;
for (Diagram *diagram : m_project->folios()) {
if (diagram->isLoaded()) {
for (Element *element : diagram->elements()) built.insert(element->uuid(), element);
} else {
const QHash<QUuid, Element *> 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<QString> {
Element *element = built.value(uuid);
if (element && element->pendingLinks().isEmpty()) {
if (element->linkedElements().isEmpty()) return std::nullopt;
return element->linkedElements().last()->actualLabel();
}
const QList<QUuid> links = database->linkedSymbols(uuid);
if (links.isEmpty()) return std::nullopt;
return label(links.last());
};
const QList<Diagram *> 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<QString, ElementInfo> WiringListExport::collectElementsInfo(const QDomElement &root) const
{
QMap<QString, ElementInfo> 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<QString> conductorDefinitionTypes;
+2
View File
@@ -6,6 +6,7 @@
#include <QMap>
#include <QList>
#include <QStringList>
#include <QDomDocument>
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<QString, ElementInfo> collectElementsInfo(const QDomElement &root) const;
QList<ConductorData> collectConductors(const QDomElement &root) const;
+57 -11
View File
@@ -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
+6 -3
View File
@@ -92,7 +92,7 @@ if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja")
--build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech>
--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=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
+558 -14
View File
@@ -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 <QSqlDatabase>
#include <QMap>
#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 <QKeyEvent>
#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 <QDomDocument>
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<QetZip::Entry> 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<QString, QStringList> 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<QUuid> 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<QetGraphicsTableItem *>(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<ElementsLocation> &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<QetGraphicsTableItem *> &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<QUuid> 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<Conductor *> 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) {
+1 -1
View File
@@ -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)
+85 -12
View File
@@ -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<QetZip::Entry> &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<QetZip::Entry> 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("<terminal_strip>"), "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<QetZip::Entry> 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<const char *>(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<QetZip::Entry> 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<QetZip::Entry> &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,"
+68 -1
View File
@@ -8,6 +8,10 @@
#include <QRegularExpression>
#include <QTemporaryDir>
#include "container/qetzip.h"
#include <sqlite3.h>
// 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<QetZip::Entry> 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<unsigned char *>(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<const char *>(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*<property [^>]*name=\"saved(filename|filepath|filenamedir|date|date-us|date-eu|time)\"[^>]*>[^<]*</property>"));
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.