mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-14 17:44:13 +02:00
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:
@@ -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"
|
||||
|
||||
@@ -10898,6 +10898,10 @@ Donnez-leur une autre numérotation d'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 > 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 > 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'activer : Configurer QElectroTech > Général > 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'élément</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QTextOrientationWidget</name>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>qet</string>
|
||||
<string>qetz</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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 "
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)))
|
||||
|
||||
@@ -131,6 +131,7 @@ class ProjectView : public QWidget
|
||||
QETResult save();
|
||||
QETResult saveAs();
|
||||
QETResult doSave();
|
||||
QETResult saveCopyForOlderVersions();
|
||||
int cleanProject();
|
||||
void updateWindowTitle();
|
||||
void updateTabTitle(DiagramView *);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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\"")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,"
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user