mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-14 17:44:13 +02:00
Compare commits
61 Commits
resolve-1461
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| dbecfae35b | |||
| 2ce508e373 | |||
| f42958d2c2 | |||
| 33a731b56f | |||
| c0a5084bc4 | |||
| 373a705bbb | |||
| af142aba90 | |||
| 8cda53f4ce | |||
| 4e8af81fc6 | |||
| 9920c13bd9 | |||
| 1768518b44 | |||
| da6eb83035 | |||
| 6751fc6f19 | |||
| bf465f9245 | |||
| 2bf73585ba | |||
| f65b24250c | |||
| a9ed99293e | |||
| 590bd6e532 | |||
| 286779a2e4 | |||
| 4c3d87e3f7 | |||
| 56c704be1f | |||
| 5975b75766 | |||
| c259cae429 | |||
| 408ef5114d | |||
| 39855e1bb5 | |||
| f48bd6717d | |||
| 8d118faf36 | |||
| 8b680af938 | |||
| 7c2a09f2bc | |||
| 132eefb8b8 | |||
| fbda162e34 | |||
| f7e0283e61 | |||
| bfa9912c5f | |||
| 4104bb8d53 | |||
| 798010cc83 | |||
| b8390c853c | |||
| a6d81a21a8 | |||
| 975ec57c74 | |||
| 9d3b9ae789 | |||
| 60489c9893 | |||
| 5c4fa8fadf | |||
| b151f52428 | |||
| 244de05421 | |||
| 9078e4e321 | |||
| 65b10b01d9 | |||
| b972dbec4b | |||
| e7fd820db6 | |||
| 8ae937007c | |||
| b891b6354a | |||
| 2f94724df3 | |||
| 8ad33f6281 | |||
| 8ea051acfc | |||
| 2f98872d7a | |||
| e89ccf2a40 | |||
| ecc7a39131 | |||
| 00e200b504 | |||
| 1c4618c8dd | |||
| 1ad3c8e2b2 | |||
| aa503aec81 | |||
| c240e4e6f7 | |||
| 561ddc29bf |
@@ -338,6 +338,9 @@ Section ""
|
|||||||
WriteRegStr HKEY_CLASSES_ROOT "qet_diagram_file\DefaultIcon" "" "$final_project_ico"
|
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"'
|
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
|
; File associations - .elmt
|
||||||
WriteRegStr HKEY_CLASSES_ROOT ".elmt" "" "qet_element_file"
|
WriteRegStr HKEY_CLASSES_ROOT ".elmt" "" "qet_element_file"
|
||||||
WriteRegStr HKEY_CLASSES_ROOT "qet_element_file" "" "Element QET"
|
WriteRegStr HKEY_CLASSES_ROOT "qet_element_file" "" "Element QET"
|
||||||
@@ -486,6 +489,7 @@ Section "Uninstall"
|
|||||||
; Remove file associations registry keys
|
; Remove file associations registry keys
|
||||||
DeleteRegKey HKEY_CLASSES_ROOT "Applications\qelectrotech.exe"
|
DeleteRegKey HKEY_CLASSES_ROOT "Applications\qelectrotech.exe"
|
||||||
DeleteRegKey HKEY_CLASSES_ROOT ".qet"
|
DeleteRegKey HKEY_CLASSES_ROOT ".qet"
|
||||||
|
DeleteRegKey HKEY_CLASSES_ROOT ".qetz"
|
||||||
DeleteRegKey HKEY_CLASSES_ROOT "qet_diagram_file"
|
DeleteRegKey HKEY_CLASSES_ROOT "qet_diagram_file"
|
||||||
DeleteRegKey HKEY_CLASSES_ROOT ".elmt"
|
DeleteRegKey HKEY_CLASSES_ROOT ".elmt"
|
||||||
DeleteRegKey HKEY_CLASSES_ROOT "qet_element_file"
|
DeleteRegKey HKEY_CLASSES_ROOT "qet_element_file"
|
||||||
|
|||||||
@@ -10898,6 +10898,10 @@ Donnez-leur une autre numérotation d'abord.</numerusform>
|
|||||||
<source>Management</source>
|
<source>Management</source>
|
||||||
<translation>Management</translation>
|
<translation>Management</translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Project QElectroTech (*.qet *.qetz)</source>
|
||||||
|
<translation>Projets QElectroTech (*.qet *.qetz)</translation>
|
||||||
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>ProjectDBModel</name>
|
<name>ProjectDBModel</name>
|
||||||
@@ -11409,6 +11413,33 @@ Voulez-vous enregistrer les modifications ?</translation>
|
|||||||
<comment>window title for a project-less ProjectView</comment>
|
<comment>window title for a project-less ProjectView</comment>
|
||||||
<translation>Projet</translation>
|
<translation>Projet</translation>
|
||||||
</message>
|
</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>
|
||||||
<context>
|
<context>
|
||||||
<name>PropertiesEditorDockWidget</name>
|
<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>
|
<comment>message box title</comment>
|
||||||
<translation>Erreur</translation>
|
<translation>Erreur</translation>
|
||||||
</message>
|
</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>
|
||||||
<context>
|
<context>
|
||||||
<name>QETElementEditor</name>
|
<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>
|
<source>Ungroup a set of terminals</source>
|
||||||
<translation>Dégrouper un ensemble de bornes</translation>
|
<translation>Dégrouper un ensemble de bornes</translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Change the information of the element</source>
|
||||||
|
<translation>Modifier les informations de l'élément</translation>
|
||||||
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>QTextOrientationWidget</name>
|
<name>QTextOrientationWidget</name>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
<key>CFBundleTypeExtensions</key>
|
<key>CFBundleTypeExtensions</key>
|
||||||
<array>
|
<array>
|
||||||
<string>qet</string>
|
<string>qet</string>
|
||||||
|
<string>qetz</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleTypeRole</key>
|
<key>CFBundleTypeRole</key>
|
||||||
<string>Editor</string>
|
<string>Editor</string>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Exec=qelectrotech %F
|
|||||||
Icon=qelectrotech
|
Icon=qelectrotech
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
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;
|
Categories=Graphics;
|
||||||
Keywords=Graphics;Science;Electricity;Engineering;
|
Keywords=Graphics;Science;Electricity;Engineering;
|
||||||
Comment=Edit electrical diagrams.
|
Comment=Edit electrical diagrams.
|
||||||
|
|||||||
@@ -34,6 +34,14 @@
|
|||||||
<comment xml:lang="zh">QElectroTech 项目文件</comment>
|
<comment xml:lang="zh">QElectroTech 项目文件</comment>
|
||||||
<glob pattern="*.qet" />
|
<glob pattern="*.qet" />
|
||||||
</mime-type>
|
</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">
|
<mime-type type="application/x-qet-element">
|
||||||
<sub-class-of type="text/xml"/>
|
<sub-class-of type="text/xml"/>
|
||||||
<comment>QElectroTech element file</comment>
|
<comment>QElectroTech element file</comment>
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ here read the model.
|
|||||||
|
|
||||||
## Tools
|
## 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 |
|
| Tool | What it answers |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `qet_project_info` | title, format version, folios with their uuids, element and conductor counts |
|
| `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():
|
if not p.is_file():
|
||||||
raise ValueError(f"no such file: {p}")
|
raise ValueError(f"no such file: {p}")
|
||||||
try:
|
try:
|
||||||
return ET.parse(p).getroot()
|
return ET.parse(_as_qet(p)).getroot()
|
||||||
except ET.ParseError as exc:
|
except ET.ParseError as exc:
|
||||||
raise ValueError(f"{p.name} is not parseable XML: {exc}") from 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:
|
def _element_info(el: ET.Element) -> dict:
|
||||||
"""The <elementInformations> bag, as a plain dict."""
|
"""The <elementInformations> bag, as a plain dict."""
|
||||||
out = {}
|
out = {}
|
||||||
@@ -796,7 +841,7 @@ def _num(v) -> float:
|
|||||||
|
|
||||||
def tool_scan(directory: str, tag: str = "conductor",
|
def tool_scan(directory: str, tag: str = "conductor",
|
||||||
attribute: str = "cable", recursive: bool = True) -> dict:
|
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`.
|
non-empty `attribute`.
|
||||||
|
|
||||||
This is the corpus question: "3190 conductors, 0 cable values across
|
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()
|
d = Path(directory).expanduser()
|
||||||
if not d.is_dir():
|
if not d.is_dir():
|
||||||
raise ValueError(f"not a directory: {d}")
|
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
|
total = non_empty = 0
|
||||||
per_file, values, unreadable = [], {}, []
|
per_file, values, unreadable = [], {}, []
|
||||||
for f in files:
|
for f in files:
|
||||||
try:
|
try:
|
||||||
root = ET.parse(f).getroot()
|
root = ET.parse(_as_qet(f)).getroot()
|
||||||
except ET.ParseError as exc:
|
except (ET.ParseError, ValueError) as exc:
|
||||||
unreadable.append({"file": f.name, "error": str(exc)})
|
unreadable.append({"file": f.name, "error": str(exc)})
|
||||||
continue
|
continue
|
||||||
n = ne = 0
|
n = ne = 0
|
||||||
@@ -3919,8 +3965,10 @@ def scripts_dir() -> Path:
|
|||||||
|
|
||||||
SERVER_INSTRUCTIONS = (
|
SERVER_INSTRUCTIONS = (
|
||||||
"This server works with QElectroTech, a free editor for electrical "
|
"This server works with QElectroTech, a free editor for electrical "
|
||||||
"diagrams. A project (.qet) holds folios (sheets) of symbols "
|
"diagrams. A project (.qet, or .qetz: the same project zipped) holds "
|
||||||
"(elements) joined by wires (conductors). Folio indexes count from 0.\n"
|
"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 "
|
"Start with qet_about: where QElectroTech keeps things, what is "
|
||||||
"switched on, the stored scripts, and house_style -- this "
|
"switched on, the stored scripts, and house_style -- this "
|
||||||
"installation's own drawing conventions if the user set any. Follow "
|
"installation's own drawing conventions if the user set any. Follow "
|
||||||
|
|||||||
@@ -6476,6 +6476,95 @@ class CorpusIntegration(unittest.TestCase):
|
|||||||
sb.close()
|
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__":
|
if __name__ == "__main__":
|
||||||
print(f"binary : {BINARY or '(none: integration tests skipped)'}")
|
print(f"binary : {BINARY or '(none: integration tests skipped)'}")
|
||||||
print(f"elements : {ELEMENTS or '(none)'}")
|
print(f"elements : {ELEMENTS or '(none)'}")
|
||||||
|
|||||||
@@ -226,9 +226,12 @@ void SearchAndReplaceWorker::replaceConductor(QList<Conductor *> list)
|
|||||||
project_->undoStack()->beginMacro(QObject::tr("Search / replace conductors properties."));
|
project_->undoStack()->beginMacro(QObject::tr("Search / replace conductors properties."));
|
||||||
for (Conductor *c : list)
|
for (Conductor *c : list)
|
||||||
{
|
{
|
||||||
ConductorProperties cp = applyChange(c->properties(), m_conductor_properties);
|
//Captured before the pushes below: QPropertyUndoCommand applies
|
||||||
|
//on push, so reading it afterwards would give back @a cp.
|
||||||
|
const ConductorProperties old_properties = c->properties();
|
||||||
|
ConductorProperties cp = applyChange(old_properties, m_conductor_properties);
|
||||||
|
|
||||||
if (cp != c->properties())
|
if (cp != old_properties)
|
||||||
{
|
{
|
||||||
QSet <Conductor *> conductors_list = c->relatedPotentialConductors(true);
|
QSet <Conductor *> conductors_list = c->relatedPotentialConductors(true);
|
||||||
conductors_list << c;
|
conductors_list << c;
|
||||||
@@ -239,6 +242,26 @@ void SearchAndReplaceWorker::replaceConductor(QList<Conductor *> list)
|
|||||||
new_value.setValue(cp);
|
new_value.setValue(cp);
|
||||||
project_->undoStack()->push(new QPropertyUndoCommand(cc, "properties", old_value, new_value));
|
project_->undoStack()->push(new QPropertyUndoCommand(cc, "properties", old_value, new_value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//The other side of a function separated terminal element
|
||||||
|
//only receives the voltage / protocol, and only when this
|
||||||
|
//replacement really changed it: the comparison is this
|
||||||
|
//replacement's own before / after (@a old_properties vs
|
||||||
|
//@a cp), never the far wire's own value, which would make
|
||||||
|
//every replacement overwrite it (@see
|
||||||
|
//Conductor::functionSeparatedConductors).
|
||||||
|
for (Conductor *cc : c->functionSeparatedConductors(true))
|
||||||
|
{
|
||||||
|
ConductorProperties separated_cp = cc->properties();
|
||||||
|
separated_cp.applyTensionProtocolChange(old_properties, cp);
|
||||||
|
if (separated_cp == cc->properties())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
QVariant old_value, new_value;
|
||||||
|
old_value.setValue(cc->properties());
|
||||||
|
new_value.setValue(separated_cp);
|
||||||
|
project_->undoStack()->push(new QPropertyUndoCommand(cc, "properties", old_value, new_value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
project_->undoStack()->endMacro();
|
project_->undoStack()->endMacro();
|
||||||
@@ -351,6 +374,25 @@ void SearchAndReplaceWorker::replaceAdvanced(
|
|||||||
new_value.setValue(new_properties);
|
new_value.setValue(new_properties);
|
||||||
project_->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value));
|
project_->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//The other side of a function separated terminal element
|
||||||
|
//only receives the voltage / protocol, and only when
|
||||||
|
//this replacement really changed it: the comparison is
|
||||||
|
//this replacement's own before / after, never the far
|
||||||
|
//wire's own value (@see
|
||||||
|
//Conductor::functionSeparatedConductors).
|
||||||
|
for (Conductor *c : conductor->functionSeparatedConductors(true))
|
||||||
|
{
|
||||||
|
ConductorProperties separated_properties = c->properties();
|
||||||
|
separated_properties.applyTensionProtocolChange(old_properties, new_properties);
|
||||||
|
if (separated_properties == c->properties())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
QVariant old_value, new_value;
|
||||||
|
old_value.setValue(c->properties());
|
||||||
|
new_value.setValue(separated_properties);
|
||||||
|
project_->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ void SearchAndReplaceWidget::fillItemsList()
|
|||||||
|
|
||||||
m_indi_text_qtwi->sortChildren(0, Qt::AscendingOrder);
|
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);
|
QTreeWidgetItem *qtwi = new QTreeWidgetItem(m_conductor_qtwi);
|
||||||
qtwi->setText(0, c->properties().text);
|
qtwi->setText(0, c->properties().text);
|
||||||
|
|||||||
@@ -1011,7 +1011,7 @@ bool TerminalStrip::fromXml(QDomElement &xml_element)
|
|||||||
auto xml_layout = xml_element.firstChildElement(QStringLiteral("layout"));
|
auto xml_layout = xml_element.firstChildElement(QStringLiteral("layout"));
|
||||||
if (!xml_layout.isNull())
|
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
|
//a strip is read with the project, before the database could
|
||||||
//tell where any symbol is
|
//tell where any symbol is
|
||||||
QSet<QUuid> terminals;
|
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"))));
|
terminals.insert(QUuid(xml_reals.at(i).toElement().attribute(QStringLiteral("element_uuid"))));
|
||||||
m_project->buildFoliosHoldingSymbols(terminals);
|
m_project->buildFoliosHoldingSymbols(terminals);
|
||||||
|
|
||||||
//Get all free elements terminal of the project
|
//Get all free elements terminal of the project: its own are on
|
||||||
const ElementProvider ep(m_project);
|
//the folios just built
|
||||||
|
const ElementProvider ep(m_project->builtFolios());
|
||||||
auto free_terminals = ep.freeTerminal();
|
auto free_terminals = ep.freeTerminal();
|
||||||
|
|
||||||
//Read each physical terminal
|
//Read each physical terminal
|
||||||
|
|||||||
@@ -321,8 +321,8 @@ TitleBlockProperties BorderTitleBlock::localTitleBlock() const
|
|||||||
/**
|
/**
|
||||||
@brief BorderTitleBlock::current
|
@brief BorderTitleBlock::current
|
||||||
@return the properties of the titleblock: for a folio placed in a
|
@return the properties of the titleblock: for a folio placed in a
|
||||||
project the project's store holds them (DB-ACCESSORS-PLAN.md stage
|
project the project's store holds them, for any other this object's
|
||||||
4.4), for any other this object's own copy.
|
own copy.
|
||||||
*/
|
*/
|
||||||
TitleBlockProperties BorderTitleBlock::current() const
|
TitleBlockProperties BorderTitleBlock::current() const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ class BorderTitleBlock : public QObject
|
|||||||
QRectF insideBorderRect() const;
|
QRectF insideBorderRect() const;
|
||||||
|
|
||||||
// methods to get title block basic data. A folio placed in a
|
// methods to get title block basic data. A folio placed in a
|
||||||
// project reads them from the project's store
|
// project reads them from the project's store, any other from its
|
||||||
// (DB-ACCESSORS-PLAN.md stage 4.4), any other from its own copy.
|
// own copy.
|
||||||
/// @return the value of the title block "Author" field
|
/// @return the value of the title block "Author" field
|
||||||
QString author() const { const TitleBlockProperties *s = stored(); return(s ? s->author : btb_author_); }
|
QString author() const { const TitleBlockProperties *s = stored(); return(s ? s->author : btb_author_); }
|
||||||
/// @return the value of the title block "Date" field
|
/// @return the value of the title block "Date" field
|
||||||
|
|||||||
+12
-9
@@ -283,7 +283,8 @@ int exportPdf(QETProject &project, const QString &output,
|
|||||||
}
|
}
|
||||||
|
|
||||||
int exportImages(QETProject &project, const QString &format,
|
int exportImages(QETProject &project, const QString &format,
|
||||||
const QString &out_dir, bool showTerminals = false)
|
const QString &out_dir, bool showTerminals = false,
|
||||||
|
bool svgSmallPictures = false)
|
||||||
{
|
{
|
||||||
const QList<Diagram *> diagrams = project.diagrams();
|
const QList<Diagram *> diagrams = project.diagrams();
|
||||||
if (diagrams.isEmpty()) {
|
if (diagrams.isEmpty()) {
|
||||||
@@ -306,8 +307,10 @@ int exportImages(QETProject &project, const QString &format,
|
|||||||
gen.setViewBox(QRect(0, 0, r.width(), r.height()));
|
gen.setViewBox(QRect(0, 0, r.width(), r.height()));
|
||||||
gen.setTitle(diagram->title());
|
gen.setTitle(diagram->title());
|
||||||
QPainter painter(&gen);
|
QPainter painter(&gen);
|
||||||
|
diagram->setSmallPictures(svgSmallPictures);
|
||||||
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()),
|
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()),
|
||||||
showTerminals);
|
showTerminals);
|
||||||
|
diagram->setSmallPictures(false);
|
||||||
painter.end();
|
painter.end();
|
||||||
} else { // png
|
} else { // png
|
||||||
QImage image(r.size(), QImage::Format_ARGB32);
|
QImage image(r.size(), QImage::Format_ARGB32);
|
||||||
@@ -870,16 +873,15 @@ int exportLinks(QETProject &project, const QString &output)
|
|||||||
int resaveProject(QETProject &project, const QString &output)
|
int resaveProject(QETProject &project, const QString &output)
|
||||||
{
|
{
|
||||||
if (QETProject::isContainerPath(output)) {
|
if (QETProject::isContainerPath(output)) {
|
||||||
const QDomDocument doc = project.toXml();
|
|
||||||
QString error;
|
QString error;
|
||||||
if (!QETProject::writeContainer(doc, output, &error)) {
|
if (!project.writeContainerFile(output, &error)) {
|
||||||
err << "Cannot write '" << output << "': " << error << "\n";
|
err << "Cannot write '" << output << "': " << error << "\n";
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
out << "Re-saved project -> " << output << "\n";
|
out << "Re-saved project -> " << output << "\n";
|
||||||
return 0;
|
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())
|
//what building them would write (QETProject::toXml())
|
||||||
QHash<int, QByteArray> kept_folios;
|
QHash<int, QByteArray> kept_folios;
|
||||||
const QDomDocument doc = project.toXml(&kept_folios);
|
const QDomDocument doc = project.toXml(&kept_folios);
|
||||||
@@ -968,15 +970,14 @@ int setTitleBlock(QETProject &project, const QString &output,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (QETProject::isContainerPath(output)) {
|
if (QETProject::isContainerPath(output)) {
|
||||||
const QDomDocument doc = project.toXml();
|
|
||||||
QString error;
|
QString error;
|
||||||
if (!QETProject::writeContainer(doc, output, &error)) {
|
if (!project.writeContainerFile(output, &error)) {
|
||||||
err << "Cannot write '" << output << "': " << error << "\n";
|
err << "Cannot write '" << output << "': " << error << "\n";
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//Folios not built (QET_LAZY_FOLIOS) written as read when that is
|
//Folios not built (lazy folios) written as read when that
|
||||||
//what building them would write (QETProject::toXml())
|
//is what building them would write (QETProject::toXml())
|
||||||
QHash<int, QByteArray> kept_folios;
|
QHash<int, QByteArray> kept_folios;
|
||||||
const QDomDocument doc = project.toXml(&kept_folios);
|
const QDomDocument doc = project.toXml(&kept_folios);
|
||||||
QFile file(output);
|
QFile file(output);
|
||||||
@@ -1014,6 +1015,7 @@ int run(const QStringList &args)
|
|||||||
const bool showTerminals = filtered.removeAll("--show-terminals") > 0;
|
const bool showTerminals = filtered.removeAll("--show-terminals") > 0;
|
||||||
const bool dxfBlocks = filtered.removeAll("--dxf-blocks") > 0;
|
const bool dxfBlocks = filtered.removeAll("--dxf-blocks") > 0;
|
||||||
const bool dxfAttributes = filtered.removeAll("--dxf-attributes") > 0;
|
const bool dxfAttributes = filtered.removeAll("--dxf-attributes") > 0;
|
||||||
|
const bool svgSmallPictures = filtered.removeAll("--svg-small-pictures") > 0;
|
||||||
// --no-slaves and --no-junctions leave rows out of --export-bom.
|
// --no-slaves and --no-junctions leave rows out of --export-bom.
|
||||||
const bool includeSlaves = filtered.removeAll("--no-slaves") == 0;
|
const bool includeSlaves = filtered.removeAll("--no-slaves") == 0;
|
||||||
const bool includeJunctions = filtered.removeAll("--no-junctions") == 0;
|
const bool includeJunctions = filtered.removeAll("--no-junctions") == 0;
|
||||||
@@ -1085,7 +1087,8 @@ int run(const QStringList &args)
|
|||||||
return resaveProject(project, output);
|
return resaveProject(project, output);
|
||||||
if (format == "settb")
|
if (format == "settb")
|
||||||
return setTitleBlock(project, output, rest.mid(2));
|
return setTitleBlock(project, output, rest.mid(2));
|
||||||
return exportImages(project, format, output, showTerminals);
|
return exportImages(project, format, output, showTerminals,
|
||||||
|
svgSmallPictures);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace CLIExport
|
} // namespace CLIExport
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ namespace CLIExport {
|
|||||||
Usage:
|
Usage:
|
||||||
qelectrotech --export-pdf <project.qet> <output.pdf> [--show-terminals]
|
qelectrotech --export-pdf <project.qet> <output.pdf> [--show-terminals]
|
||||||
qelectrotech --export-png <project.qet> <output_dir> [--show-terminals]
|
qelectrotech --export-png <project.qet> <output_dir> [--show-terminals]
|
||||||
qelectrotech --export-svg <project.qet> <output_dir> [--show-terminals]
|
qelectrotech --export-svg <project.qet> <output_dir> [--show-terminals] [--svg-small-pictures]
|
||||||
qelectrotech --export-dxf <project.qet> <output_dir> [--show-terminals] [--dxf-blocks] [--dxf-attributes]
|
qelectrotech --export-dxf <project.qet> <output_dir> [--show-terminals] [--dxf-blocks] [--dxf-attributes]
|
||||||
qelectrotech --export-cables <project.qet> <output.csv>
|
qelectrotech --export-cables <project.qet> <output.csv>
|
||||||
qelectrotech --export-wires <project.qet> <output.csv>
|
qelectrotech --export-wires <project.qet> <output.csv>
|
||||||
@@ -71,6 +71,9 @@ namespace CLIExport {
|
|||||||
--dxf-attributes: as --dxf-blocks, and a symbol's texts (its label,
|
--dxf-attributes: as --dxf-blocks, and a symbol's texts (its label,
|
||||||
function...) are attributes of its INSERT. LibreCAD does not
|
function...) are attributes of its INSERT. LibreCAD does not
|
||||||
show attributes.
|
show attributes.
|
||||||
|
--svg-small-pictures: in an SVG, each picture at most four times the
|
||||||
|
size it is drawn at instead of its full resolution: a much
|
||||||
|
smaller file, still sharp when printed.
|
||||||
cables: wiring list (one row per conductor) as CSV.
|
cables: wiring list (one row per conductor) as CSV.
|
||||||
wires: list of distinct wire numbers as CSV.
|
wires: list of distinct wire numbers as CSV.
|
||||||
bom: bill of materials (one row per element) as CSV.
|
bom: bill of materials (one row per element) as CSV.
|
||||||
|
|||||||
@@ -62,33 +62,80 @@ namespace ConductorMultiEdit
|
|||||||
@brief targets
|
@brief targets
|
||||||
@param selected : the edited conductors
|
@param selected : the edited conductors
|
||||||
@param apply_all : true to add every conductor of their potentials
|
@param apply_all : true to add every conductor of their potentials
|
||||||
|
@param function_separated : set to the conductors that share the
|
||||||
|
potential of an edited one only by crossing a function separated
|
||||||
|
terminal element -- conductors undo() then gives the voltage /
|
||||||
|
protocol only, and no other property. nullptr, the default, leaves
|
||||||
|
those conductors out of the returned list entirely.
|
||||||
|
|
||||||
|
A conductor the user picked themselves is never part of it: the
|
||||||
|
selection is the edit reaching that conductor, so it takes the
|
||||||
|
full edit however remote it is from the other selected ones --
|
||||||
|
picking a wire on each side of a separation and editing them is
|
||||||
|
exactly that. Nor is a conductor that belongs to a potential, on
|
||||||
|
any path, however far that potential is reached.
|
||||||
@return each conductor an edit reaches, once
|
@return each conductor an edit reaches, once
|
||||||
*/
|
*/
|
||||||
template <typename T>
|
template <typename T>
|
||||||
QList<T *> targets(const QList<T *> &selected, bool apply_all)
|
QList<T *> targets(const QList<T *> &selected, bool apply_all,
|
||||||
|
QSet<T *> *function_separated = nullptr)
|
||||||
{
|
{
|
||||||
QList<T *> list;
|
QList<T *> list;
|
||||||
QSet<T *> seen;
|
QSet<T *> seen;
|
||||||
|
//The explicitly picked ones, so a separation met while walking
|
||||||
|
//the list cannot demote a wire the user asked to edit.
|
||||||
|
QSet<T *> chosen;
|
||||||
|
for (T *conductor : selected)
|
||||||
|
if (conductor)
|
||||||
|
chosen.insert(conductor);
|
||||||
|
//What the potentials reach, and what only a function separation
|
||||||
|
//reaches. A conductor can be in both -- wired normally on one
|
||||||
|
//path and across a separation on another -- and then belongs to
|
||||||
|
//the potential.
|
||||||
|
QSet<T *> potential_reached, separated_reached;
|
||||||
|
|
||||||
for (T *conductor : selected)
|
for (T *conductor : selected)
|
||||||
{
|
{
|
||||||
//Already reached through an earlier conductor's potential:
|
if (!conductor)
|
||||||
//its own potential is the same one.
|
|
||||||
if (!conductor || seen.contains(conductor))
|
|
||||||
continue;
|
continue;
|
||||||
seen.insert(conductor);
|
//Already reached through an earlier conductor's potential or
|
||||||
list << conductor;
|
//separation: its own potential adds nothing new to walk over
|
||||||
if (!apply_all)
|
//-- unless the user picked it, because a separation may have
|
||||||
|
//put it in the list before its potential was ever reached.
|
||||||
|
const bool fresh = !seen.contains(conductor);
|
||||||
|
if (fresh)
|
||||||
|
{
|
||||||
|
seen.insert(conductor);
|
||||||
|
list << conductor;
|
||||||
|
}
|
||||||
|
if (!apply_all || (!fresh && !chosen.contains(conductor)))
|
||||||
continue;
|
continue;
|
||||||
const QSet<T *> potential = conductor->relatedPotentialConductors();
|
const QSet<T *> potential = conductor->relatedPotentialConductors();
|
||||||
for (T *other : potential)
|
for (T *other : potential)
|
||||||
{
|
{
|
||||||
|
potential_reached.insert(other);
|
||||||
if (!seen.contains(other))
|
if (!seen.contains(other))
|
||||||
{
|
{
|
||||||
seen.insert(other);
|
seen.insert(other);
|
||||||
list << other;
|
list << other;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (function_separated)
|
||||||
|
{
|
||||||
|
const QSet<T *> separated = conductor->functionSeparatedConductors();
|
||||||
|
for (T *other : separated)
|
||||||
|
{
|
||||||
|
separated_reached.insert(other);
|
||||||
|
if (!seen.contains(other))
|
||||||
|
{
|
||||||
|
seen.insert(other);
|
||||||
|
list << other;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (function_separated)
|
||||||
|
*function_separated = separated_reached - potential_reached - chosen;
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,13 +144,18 @@ namespace ConductorMultiEdit
|
|||||||
@param targets : the conductors to change, see targets()
|
@param targets : the conductors to change, see targets()
|
||||||
@param shown : the properties as the panel showed them
|
@param shown : the properties as the panel showed them
|
||||||
@param edited : the properties after the user's edit
|
@param edited : the properties after the user's edit
|
||||||
|
@param function_separated : the targets of targets() that only a
|
||||||
|
function separated terminal element links to the edited
|
||||||
|
conductors: they get the voltage / protocol and nothing else
|
||||||
|
(@see ConductorProperties::applyTensionProtocolChange)
|
||||||
@return one undo step giving each target the fields that differ
|
@return one undo step giving each target the fields that differ
|
||||||
between shown and edited, or nullptr if no conductor changes
|
between shown and edited, or nullptr if no conductor changes
|
||||||
*/
|
*/
|
||||||
template <typename T>
|
template <typename T>
|
||||||
QUndoCommand *undo(const QList<T *> &targets,
|
QUndoCommand *undo(const QList<T *> &targets,
|
||||||
const ConductorProperties &shown,
|
const ConductorProperties &shown,
|
||||||
const ConductorProperties &edited)
|
const ConductorProperties &edited,
|
||||||
|
const QSet<T *> &function_separated = QSet<T *>())
|
||||||
{
|
{
|
||||||
if (edited == shown)
|
if (edited == shown)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -114,7 +166,10 @@ namespace ConductorMultiEdit
|
|||||||
{
|
{
|
||||||
const ConductorProperties old_properties = conductor->properties();
|
const ConductorProperties old_properties = conductor->properties();
|
||||||
ConductorProperties properties = old_properties;
|
ConductorProperties properties = old_properties;
|
||||||
properties.applyChanges(shown, edited);
|
if (function_separated.contains(conductor))
|
||||||
|
properties.applyTensionProtocolChange(shown, edited);
|
||||||
|
else
|
||||||
|
properties.applyChanges(shown, edited);
|
||||||
if (properties == old_properties)
|
if (properties == old_properties)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,12 @@
|
|||||||
#include "qetproject.h"
|
#include "qetproject.h"
|
||||||
#include "qetapp.h"
|
#include "qetapp.h"
|
||||||
#include "diagram.h"
|
#include "diagram.h"
|
||||||
#include "diagramcontent.h"
|
|
||||||
#include "qetgraphicsitem/conductor.h"
|
#include "qetgraphicsitem/conductor.h"
|
||||||
#include "qetgraphicsitem/conductortextitem.h"
|
#include "qetgraphicsitem/conductortextitem.h"
|
||||||
#include "qetgraphicsitem/element.h"
|
#include "qetgraphicsitem/element.h"
|
||||||
#include "qetgraphicsitem/terminal.h"
|
#include "qetgraphicsitem/terminal.h"
|
||||||
|
#include "dataBase/projectdatabase.h"
|
||||||
|
#include "properties/elementdata.h"
|
||||||
|
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
|
|
||||||
@@ -114,10 +115,30 @@ void ConductorNumExport::fillHash()
|
|||||||
{
|
{
|
||||||
//We used this rx to avoid insert num composed only withe white space.
|
//We used this rx to avoid insert num composed only withe white space.
|
||||||
QRegularExpression rx("^ *$");
|
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);
|
if (wire.text.isEmpty() || wire.text.contains(rx)) {
|
||||||
for (Conductor *c : dc.conductors())
|
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();
|
QString num = c->textItem()->toPlainText();
|
||||||
if (num.isEmpty() || num.contains(rx)) {
|
if (num.isEmpty() || num.contains(rx)) {
|
||||||
|
|||||||
@@ -851,6 +851,32 @@ void ConductorProperties::applyChanges(const ConductorProperties &before,
|
|||||||
singleLineProperties.setPhasesCount(slp_after.phasesCount());
|
singleLineProperties.setPhasesCount(slp_after.phasesCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief ConductorProperties::applyTensionProtocolChange
|
||||||
|
Copy into this the voltage / protocol when it differs between before
|
||||||
|
and after, and leave every other attribute as it is. This is the one
|
||||||
|
property a function separated terminal element lets through
|
||||||
|
(@see Conductor::functionSeparatedConductors): the voltage /
|
||||||
|
protocol describes the electrical node itself, while the text, the
|
||||||
|
function, the wire color... describe one side of the separation and
|
||||||
|
stay local to it.
|
||||||
|
|
||||||
|
Both arguments describe one edit -- what was shown to the user, and
|
||||||
|
what the user made of it -- so that only an edit which really
|
||||||
|
changed the voltage / protocol passes it on. Passing the far wire's
|
||||||
|
own properties as @a before instead would make every edit overwrite
|
||||||
|
its voltage: the two sides differ on that field just as often as
|
||||||
|
not, so the comparison would hold no matter what was edited.
|
||||||
|
@param before : the properties as they were shown to the user
|
||||||
|
@param after : the properties after the user's edit
|
||||||
|
*/
|
||||||
|
void ConductorProperties::applyTensionProtocolChange(const ConductorProperties &before,
|
||||||
|
const ConductorProperties &after)
|
||||||
|
{
|
||||||
|
if (before.m_tension_protocol != after.m_tension_protocol)
|
||||||
|
m_tension_protocol = after.m_tension_protocol;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief ConductorProperties::defaultProperties
|
@brief ConductorProperties::defaultProperties
|
||||||
@return the default properties stored in the setting file
|
@return the default properties stored in the setting file
|
||||||
|
|||||||
@@ -139,6 +139,8 @@ class ConductorProperties
|
|||||||
void applyForEqualAttributes(QList<ConductorProperties> list);
|
void applyForEqualAttributes(QList<ConductorProperties> list);
|
||||||
void applyChanges(const ConductorProperties &before,
|
void applyChanges(const ConductorProperties &before,
|
||||||
const ConductorProperties &after);
|
const ConductorProperties &after);
|
||||||
|
void applyTensionProtocolChange(const ConductorProperties &before,
|
||||||
|
const ConductorProperties &after);
|
||||||
|
|
||||||
static ConductorProperties defaultProperties();
|
static ConductorProperties defaultProperties();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
# 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 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`
|
||||||
|
(the database) and `qetzip.cpp` (reading and writing zip archives).
|
||||||
|
|
||||||
|
## 1. Principles
|
||||||
|
|
||||||
|
- **One writer.** A `.qet` and a `.qetz` are written from the same project
|
||||||
|
document. Saving a `.qetz` splits that document into files and a database;
|
||||||
|
opening one puts it back together, and the program reads it exactly as a
|
||||||
|
`.qet`. Opening a `.qetz` and saving it as `.qet` gives the file a plain
|
||||||
|
`.qet` save gives.
|
||||||
|
- **Each fact once.** A value moved into the database is taken out of the
|
||||||
|
folio file. Nothing is stored in both.
|
||||||
|
- **What a thing is, against where it is drawn.** The database holds the
|
||||||
|
engineering data; the folio files hold the drawing:
|
||||||
|
|
||||||
|
| | database | folio file |
|
||||||
|
|---|---|---|
|
||||||
|
| Symbol | its information (label, comment, …), prefix, links | which symbol it is (`type`), position, rotation |
|
||||||
|
| Label shown on a folio | the value | the text item: position, font, rotation |
|
||||||
|
| Wire | its ends, number, function, voltage, colour, section | its path, the number's text position and font |
|
||||||
|
| Folio | title-block fields and custom variables | everything drawn on it |
|
||||||
|
| 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="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) |
|
||||||
|
| `elements/<categories>/<name>` | the project's symbol definitions, as in the symbol collection |
|
||||||
|
| `titleblocks/<name>.titleblock` | the project's title-block templates |
|
||||||
|
| `images/<sha256>.png` | each folio picture once, as a file instead of base64 text (`.bin` if it is not a PNG) |
|
||||||
|
|
||||||
|
Names that would collide get `~2`, `~3`… before the extension. A picture
|
||||||
|
in a folio file keeps the name of its file in the attribute `qetz-file`.
|
||||||
|
|
||||||
|
A reader refuses the whole archive if an entry is encrypted, uses a
|
||||||
|
compression method it cannot read, has a name that is absolute, holds `..`,
|
||||||
|
`\` or `:`, or repeats another entry's name, or if the archive holds more
|
||||||
|
than 100 000 entries or more than 2 GiB once unpacked. Entries the format
|
||||||
|
does not name are ignored.
|
||||||
|
|
||||||
|
## 3. Versions
|
||||||
|
|
||||||
|
`manifest.xml` gives two numbers: `format`, the format written, and
|
||||||
|
`min-reader`, the oldest format allowed to read the file.
|
||||||
|
|
||||||
|
- A reader opens a file whose `min-reader` is not above its own format.
|
||||||
|
- If `format` is above its own, the project opens **read-only**, with a
|
||||||
|
message: saving it could drop what that reader does not know. *Save As*
|
||||||
|
writes a copy in the reader's own format.
|
||||||
|
- If `min-reader` is above its own format, the file is refused with a
|
||||||
|
message.
|
||||||
|
|
||||||
|
The database repeats the format in `meta` (key `format`) and in
|
||||||
|
`PRAGMA user_version`.
|
||||||
|
|
||||||
|
**Rules for a new format:**
|
||||||
|
|
||||||
|
1. New data goes in a new column or table. An older reader skips both.
|
||||||
|
2. A change an older reader would misread raises `min-reader`.
|
||||||
|
3. Every format keeps a saved file in `tests/qttest/fixtures` that must
|
||||||
|
keep opening.
|
||||||
|
|
||||||
|
| 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 | #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);
|
||||||
|
CREATE TABLE project_property (ord INTEGER PRIMARY KEY, name TEXT, value TEXT, show INTEGER, attrs TEXT);
|
||||||
|
CREATE TABLE folio (uuid TEXT PRIMARY KEY, pos INTEGER NOT NULL UNIQUE,
|
||||||
|
"title" TEXT, "author" TEXT, "date" TEXT, "folio" TEXT, "indexrev" TEXT, "filename" TEXT,
|
||||||
|
"locmach" TEXT, "plant" TEXT, "auto_page_num" TEXT, props_attrs TEXT);
|
||||||
|
CREATE TABLE folio_property (folio_uuid TEXT NOT NULL REFERENCES folio, ord INTEGER NOT NULL,
|
||||||
|
name TEXT, value TEXT, show INTEGER, attrs TEXT, PRIMARY KEY (folio_uuid, ord));
|
||||||
|
CREATE TABLE element (uuid TEXT PRIMARY KEY, folio_uuid TEXT NOT NULL REFERENCES folio,
|
||||||
|
"prefix" TEXT, "freezeLabel" INTEGER, info_attrs TEXT, links_attrs TEXT);
|
||||||
|
CREATE TABLE element_info (element_uuid TEXT NOT NULL REFERENCES element, ord INTEGER NOT NULL,
|
||||||
|
name TEXT, value TEXT, show INTEGER, attrs TEXT, PRIMARY KEY (element_uuid, ord));
|
||||||
|
CREATE TABLE link (element_uuid TEXT NOT NULL REFERENCES element, ord INTEGER NOT NULL,
|
||||||
|
linked_uuid TEXT, attrs TEXT, PRIMARY KEY (element_uuid, ord));
|
||||||
|
CREATE TABLE conductor (uuid TEXT PRIMARY KEY, folio_uuid TEXT NOT NULL REFERENCES folio,
|
||||||
|
"element1" TEXT, "terminal1" TEXT, "element2" TEXT, "terminal2" TEXT, "num" TEXT,
|
||||||
|
"formula" TEXT, "function" TEXT, "tension_protocol" TEXT, "conductor_color" TEXT,
|
||||||
|
"conductor_section" TEXT, "cable" TEXT, "bus" TEXT, "freezeLabel" INTEGER);
|
||||||
|
CREATE TABLE terminal_strip (uuid TEXT PRIMARY KEY, ord INTEGER NOT NULL UNIQUE,
|
||||||
|
installation TEXT, location TEXT, name TEXT, comment TEXT, description TEXT);
|
||||||
|
CREATE TABLE strip_terminal (strip_uuid TEXT NOT NULL REFERENCES terminal_strip,
|
||||||
|
pos INTEGER NOT NULL, level INTEGER NOT NULL, element_uuid TEXT,
|
||||||
|
PRIMARY KEY (strip_uuid, pos, level));
|
||||||
|
CREATE TABLE strip_bridge (uuid TEXT PRIMARY KEY, strip_uuid TEXT NOT NULL REFERENCES terminal_strip,
|
||||||
|
ord INTEGER NOT NULL, color TEXT);
|
||||||
|
CREATE TABLE strip_bridge_terminal (bridge_uuid TEXT NOT NULL REFERENCES strip_bridge,
|
||||||
|
ord INTEGER NOT NULL, element_uuid TEXT, PRIMARY KEY (bridge_uuid, ord));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 What each table holds
|
||||||
|
|
||||||
|
| Table | One row per | Comes from |
|
||||||
|
|---|---|---|
|
||||||
|
| `meta` | setting of the file | `format`; `props_attrs` (attributes of the project's `<properties>`); `terminal_strips` = `1` when the strips are in the database |
|
||||||
|
| `project_property` | project property (project-wide variable) | `<project><properties><property>` |
|
||||||
|
| `folio` | folio, in folio order (`pos` from 1) | the title-block attributes of `<diagram>` |
|
||||||
|
| `folio_property` | custom title-block variable of a folio | `<diagram><properties><property>` |
|
||||||
|
| `element` | symbol placed on a folio | `<element>`: its label prefix and frozen-label flag |
|
||||||
|
| `element_info` | piece of symbol information, in order | `<elementInformations><elementInformation>` |
|
||||||
|
| `link` | cross-reference link of a symbol, in order | `<links_uuids><link_uuid>` |
|
||||||
|
| `conductor` | wire | `<conductor>`: its two ends (symbol uuid and terminal), number, formula, function, voltage/protocol, colour, section, cable, bus, frozen-label flag |
|
||||||
|
| `terminal_strip`, `strip_terminal`, `strip_bridge`, `strip_bridge_terminal` | terminal strip, terminal, bridge, bridged terminal | `<terminal_strips>` |
|
||||||
|
|
||||||
|
A value is NULL when the XML has no such attribute.
|
||||||
|
|
||||||
|
- `freezeLabel` is 1 for `true`, 0 for `false`. Any other text stays as an
|
||||||
|
attribute in the folio file.
|
||||||
|
- `show` is 0 or 1. Any other text goes in `attrs`.
|
||||||
|
- `attrs`, `props_attrs`, `info_attrs` and `links_attrs` are JSON objects of
|
||||||
|
the attributes that have no column, and the entry's tag when it is not the
|
||||||
|
usual one. In a project QElectroTech saves they are all NULL. They hold
|
||||||
|
something only for older or hand-edited files. Whitespace between
|
||||||
|
entries, which only a hand-edited file has, is kept in them as
|
||||||
|
`qetz-ws`.
|
||||||
|
|
||||||
|
### 4.3 Keys
|
||||||
|
|
||||||
|
Every row is keyed by its item's **uuid**, which stays in the folio file:
|
||||||
|
that is how a folio file's item finds its row. Folios, symbols and wires
|
||||||
|
are keyed this way.
|
||||||
|
|
||||||
|
An item with no uuid of its own (none, or one another item of the same kind
|
||||||
|
shares) gets a key made from the item itself: a version 5 uuid in the
|
||||||
|
namespace `{3f0c6c1e-5b8e-4b8a-9d55-6b2f5f0f9a41}` from its XML text, how
|
||||||
|
many identical items came before it, and a counter that avoids taken keys.
|
||||||
|
The key is the same on every save and never depends on where the item is.
|
||||||
|
It is written on the item as `qetz-key` (a symbol or wire) or `qetz-folio`
|
||||||
|
(a folio).
|
||||||
|
|
||||||
|
### 4.4 Markers
|
||||||
|
|
||||||
|
A block moved into the database leaves `<qetz-db block="…"/>` where it was:
|
||||||
|
`properties`, `elementInformations`, `links_uuids` or `terminal_strips`.
|
||||||
|
Reading puts the block back where its marker is; a block without a marker
|
||||||
|
was never moved.
|
||||||
|
|
||||||
|
Only a block that can be rebuilt exactly is moved: entries of the expected
|
||||||
|
tag with plain text values. Terminal strips are moved only if their rows
|
||||||
|
rebuild the same XML. Anything else stays in the folio file or
|
||||||
|
`project.xml` unchanged.
|
||||||
|
|
||||||
|
## 5. Reading a file safely
|
||||||
|
|
||||||
|
A `.qetz` is untrusted. A reader:
|
||||||
|
|
||||||
|
- runs nothing from the database's schema (`PRAGMA trusted_schema = OFF`,
|
||||||
|
read-only) and refuses a database where any table of section 4.1 is not a
|
||||||
|
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 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 <QHash>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QUuid>
|
||||||
|
|
||||||
const char QetContainer::MimeType[] = "application/x-qelectrotech-project+zip";
|
const char QetContainer::MimeType[] = "application/x-qelectrotech-project+zip";
|
||||||
|
|
||||||
@@ -93,6 +95,20 @@ QList<QDomElement> childElements(const QDomElement &parent, const QString &tag)
|
|||||||
return list;
|
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>
|
//The collection's symbol definitions, each to elements/<categories>/<name>
|
||||||
void splitCollection(QDomDocument &document, const QDomElement &category, const QString &path,
|
void splitCollection(QDomDocument &document, const QDomElement &category, const QString &path,
|
||||||
QList<QetZip::Entry> *parts, QSet<QString> *taken)
|
QList<QetZip::Entry> *parts, QSet<QString> *taken)
|
||||||
@@ -153,15 +169,93 @@ bool QetContainer::isZip(const QByteArray &content)
|
|||||||
/**
|
/**
|
||||||
@brief QetContainer::readDocument
|
@brief QetContainer::readDocument
|
||||||
The project document held in @p content: unzipped and joined for a
|
The project document held in @p content: unzipped and joined for a
|
||||||
.qetz, parsed for a .qet.
|
.qetz, parsed for a .qet. @p format, if any: the .qetz's format, 0
|
||||||
|
for a .qet.
|
||||||
*/
|
*/
|
||||||
bool QetContainer::readDocument(const QByteArray &content, QDomDocument *document, QString *error)
|
bool QetContainer::readDocument(const QByteArray &content, QDomDocument *document,
|
||||||
|
QString *error, Pictures *pictures, int *format)
|
||||||
{
|
{
|
||||||
|
if (format) *format = 0;
|
||||||
if (!isZip(content))
|
if (!isZip(content))
|
||||||
return parse(content, document, error);
|
return parse(content, document, error);
|
||||||
QList<QetZip::Entry> entries;
|
QList<QetZip::Entry> entries;
|
||||||
return QetZip::fromBytes(content, &entries, error)
|
return QetZip::fromBytes(content, &entries, error)
|
||||||
&& join(entries, document, error);
|
&& join(entries, document, error, nullptr, pictures, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,7 +266,7 @@ bool QetContainer::readDocument(const QByteArray &content, QDomDocument *documen
|
|||||||
set if @p project is not a project
|
set if @p project is not a project
|
||||||
*/
|
*/
|
||||||
QList<QetZip::Entry> QetContainer::split(const QDomDocument &project, const QString &writer,
|
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();
|
QDomDocument document = project.cloneNode(true).toDocument();
|
||||||
QDomElement root = document.documentElement();
|
QDomElement root = document.documentElement();
|
||||||
@@ -182,7 +276,34 @@ QList<QetZip::Entry> QetContainer::split(const QDomDocument &project, const QStr
|
|||||||
}
|
}
|
||||||
|
|
||||||
//The engineering data out of the document, into project.sqlite
|
//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())
|
if (database.isEmpty())
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
@@ -191,7 +312,36 @@ QList<QetZip::Entry> QetContainer::split(const QDomDocument &project, const QStr
|
|||||||
|
|
||||||
//Folios, each with its pictures taken out
|
//Folios, each with its pictures taken out
|
||||||
int folio = 0;
|
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
|
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")))
|
for (QDomElement diagram : childElements(root, QStringLiteral("diagram")))
|
||||||
{
|
{
|
||||||
++folio;
|
++folio;
|
||||||
@@ -264,8 +414,13 @@ QList<QetZip::Entry> QetContainer::split(const QDomDocument &project, const QStr
|
|||||||
Refused, with @p error set: not a .qetz, made for a later QElectroTech
|
Refused, with @p error set: not a .qetz, made for a later QElectroTech
|
||||||
(the manifest's min-reader), or a part missing or unreadable. Entries
|
(the manifest's min-reader), or a part missing or unreadable. Entries
|
||||||
nothing names are ignored, so a later format may add some.
|
nothing names are ignored, so a later format may add some.
|
||||||
|
With @p pictures, the pictures stay out of the document: see the
|
||||||
|
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)
|
bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *project, QString *error,
|
||||||
|
QStringList *folio_files, Pictures *pictures, int *format)
|
||||||
{
|
{
|
||||||
QHash<QString, QByteArray> files;
|
QHash<QString, QByteArray> files;
|
||||||
for (const QetZip::Entry &entry : entries)
|
for (const QetZip::Entry &entry : entries)
|
||||||
@@ -286,6 +441,8 @@ bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *proje
|
|||||||
.arg(Format));
|
.arg(Format));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (format)
|
||||||
|
*format = manifest.documentElement().attribute(QStringLiteral("format")).toInt();
|
||||||
|
|
||||||
if (!parse(files.value(QStringLiteral("project.xml")), project, error))
|
if (!parse(files.value(QStringLiteral("project.xml")), project, error))
|
||||||
return false;
|
return false;
|
||||||
@@ -297,6 +454,9 @@ bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *proje
|
|||||||
for (QDomElement part : placeholders)
|
for (QDomElement part : placeholders)
|
||||||
{
|
{
|
||||||
const QString file = part.attribute(FileAttribute);
|
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)) {
|
if (!files.contains(file)) {
|
||||||
setError(error, QStringLiteral("part \"%1\" is missing").arg(file));
|
setError(error, QStringLiteral("part \"%1\" is missing").arg(file));
|
||||||
return false;
|
return false;
|
||||||
@@ -310,7 +470,8 @@ bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *proje
|
|||||||
part.parentNode().replaceChild(project->importNode(document.documentElement(), true), part);
|
part.parentNode().replaceChild(project->importNode(document.documentElement(), true), part);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Pictures back to base64, as the image's first child
|
//Pictures back to base64, as the image's first child, or into
|
||||||
|
//pictures
|
||||||
const QDomNodeList images = project->elementsByTagName(QStringLiteral("image"));
|
const QDomNodeList images = project->elementsByTagName(QStringLiteral("image"));
|
||||||
for (int i = 0 ; i < images.size() ; ++i)
|
for (int i = 0 ; i < images.size() ; ++i)
|
||||||
{
|
{
|
||||||
@@ -321,6 +482,10 @@ bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *proje
|
|||||||
setError(error, QStringLiteral("picture \"%1\" is missing").arg(file));
|
setError(error, QStringLiteral("picture \"%1\" is missing").arg(file));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (pictures) {
|
||||||
|
pictures->insert(file, files.value(file));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
image.removeAttribute(ImageFileAttribute);
|
image.removeAttribute(ImageFileAttribute);
|
||||||
image.insertBefore(project->createTextNode(QString::fromLatin1(files.value(file).toBase64())),
|
image.insertBefore(project->createTextNode(QString::fromLatin1(files.value(file).toBase64())),
|
||||||
image.firstChild());
|
image.firstChild());
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include "qetzip.h"
|
#include "qetzip.h"
|
||||||
|
|
||||||
#include <QDomDocument>
|
#include <QDomDocument>
|
||||||
|
#include <QHash>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief The QetContainer namespace
|
@brief The QetContainer namespace
|
||||||
@@ -31,7 +32,7 @@
|
|||||||
manifest.xml format and oldest format allowed to read it
|
manifest.xml format and oldest format allowed to read it
|
||||||
project.xml the project, each part replaced by
|
project.xml the project, each part replaced by
|
||||||
<qetz-part file="..."/>
|
<qetz-part file="..."/>
|
||||||
project.sqlite the engineering data (format 2,
|
project.sqlite the engineering data (format 2 on,
|
||||||
QetContainerDb): title-block fields, symbol
|
QetContainerDb): title-block fields, symbol
|
||||||
information, links, wires
|
information, links, wires
|
||||||
folios/<uuid>.xml one per folio
|
folios/<uuid>.xml one per folio
|
||||||
@@ -41,26 +42,54 @@
|
|||||||
|
|
||||||
join(split(d)) is d. Nothing here knows what the parts mean: the
|
join(split(d)) is d. Nothing here knows what the parts mean: the
|
||||||
project is read and written by the same code as a .qet file.
|
project is read and written by the same code as a .qet file.
|
||||||
|
|
||||||
|
Given a Pictures map, join() leaves the pictures out of the document:
|
||||||
|
each <image> keeps the name of its file, pictureFile(), and the bytes
|
||||||
|
go in the map. Turning them back into base64 text costs seconds on a
|
||||||
|
project with large pictures, and the folio would only decode it again.
|
||||||
|
|
||||||
|
The format is written down in QETZ-FORMAT.md, beside this file.
|
||||||
*/
|
*/
|
||||||
namespace QetContainer
|
namespace QetContainer
|
||||||
{
|
{
|
||||||
extern const char MimeType[];
|
extern const char MimeType[];
|
||||||
|
/// Picture bytes by file name, see join()
|
||||||
|
using Pictures = QHash<QString, QByteArray>;
|
||||||
/// The format written; files of a later format are refused by join()
|
/// The format written; files of a later format are refused by join()
|
||||||
/// unless their min-reader allows this one.
|
/// unless their min-reader allows this one.
|
||||||
constexpr int Format = 2;
|
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.
|
||||||
|
/// 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,
|
QList<QetZip::Entry> split(const QDomDocument &project,
|
||||||
const QString &writer = QString(),
|
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,
|
bool join(const QList<QetZip::Entry> &entries, QDomDocument *project,
|
||||||
QString *error = 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)
|
/// Parse @p xml as a .qet is read (QETProject::openFile)
|
||||||
bool parse(const QByteArray &xml, QDomDocument *document, QString *error = nullptr);
|
bool parse(const QByteArray &xml, QDomDocument *document, QString *error = nullptr);
|
||||||
/// true if @p content is a zip, so a .qetz rather than a .qet
|
/// true if @p content is a zip, so a .qetz rather than a .qet
|
||||||
bool isZip(const QByteArray &content);
|
bool isZip(const QByteArray &content);
|
||||||
/// The project document in @p content, a .qet or a .qetz
|
/// The project document in @p content, a .qet or a .qetz
|
||||||
bool readDocument(const QByteArray &content, QDomDocument *document, QString *error = nullptr);
|
bool readDocument(const QByteArray &content, QDomDocument *document,
|
||||||
|
QString *error = nullptr, Pictures *pictures = nullptr, int *format = nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // QETCONTAINER_H
|
#endif // QETCONTAINER_H
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -30,15 +30,20 @@
|
|||||||
drawn. extract() takes them out of the document into a database,
|
drawn. extract() takes them out of the document into a database,
|
||||||
restore() puts them back, so that restore(extract(d)) is d.
|
restore() puts them back, so that restore(extract(d)) is d.
|
||||||
|
|
||||||
Each moved item leaves its key in the document (qetz-key, qetz-folio):
|
A row's key is its item's uuid, which stays in the folio file. An item
|
||||||
its uuid where that is unique, else a made-up one. A block of a shape
|
without a uuid of its own -- none, or one another item shares -- gets a
|
||||||
|
uuid made from what it holds, left in the document as qetz-key or
|
||||||
|
qetz-folio. A block of a shape
|
||||||
this does not model -- a value holding elements, say -- stays in the
|
this does not model -- a value holding elements, say -- stays in the
|
||||||
document, so nothing can be lost by moving.
|
document, so nothing can be lost by moving.
|
||||||
*/
|
*/
|
||||||
namespace QetContainerDb
|
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);
|
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
|
#endif // QETCONTAINERDB_H
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QSaveFile>
|
#include <QSaveFile>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
|
#include <QtEndian>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
void setError(QString *error, const QString &text)
|
void setError(QString *error, const QString &text)
|
||||||
@@ -44,6 +45,42 @@ bool safeName(const QString &name)
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Built with MINIZ_NO_TIME, miniz leaves each entry's DOS date at 0,
|
||||||
|
//day 0 of month 0, which is no date: unzip shows 30 Nov 1979, other
|
||||||
|
//tools 13 Dec 1901. Give every entry 1 Jan 1980 00:00 instead, the
|
||||||
|
//first valid DOS date, so the bytes still do not depend on the time.
|
||||||
|
constexpr quint16 EntryDosDate = (1 << 5) | 1;
|
||||||
|
|
||||||
|
bool setEntryDates(QByteArray *bytes)
|
||||||
|
{
|
||||||
|
char *data = bytes->data();
|
||||||
|
mz_zip_archive zip;
|
||||||
|
mz_zip_zero_struct(&zip);
|
||||||
|
if (!mz_zip_reader_init_mem(&zip, data, size_t(bytes->size()), 0)) return false;
|
||||||
|
|
||||||
|
bool ok = true;
|
||||||
|
const mz_uint count = mz_zip_reader_get_num_files(&zip);
|
||||||
|
for (mz_uint i = 0 ; i < count && ok ; ++i)
|
||||||
|
{
|
||||||
|
mz_zip_archive_file_stat stat;
|
||||||
|
if (!mz_zip_reader_file_stat(&zip, i, &stat)) {
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
//date at offset 12 of the local header, 14 of the central one
|
||||||
|
const quint64 local = stat.m_local_header_ofs + 12;
|
||||||
|
const quint64 central = zip.m_central_directory_file_ofs + stat.m_central_dir_ofs + 14;
|
||||||
|
if (local + 2 > quint64(bytes->size()) || central + 2 > quint64(bytes->size())) {
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
qToLittleEndian<quint16>(EntryDosDate, data + local);
|
||||||
|
qToLittleEndian<quint16>(EntryDosDate, data + central);
|
||||||
|
}
|
||||||
|
mz_zip_reader_end(&zip);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,8 +124,12 @@ QByteArray QetZip::toBytes(const QList<Entry> &entries, QString *error)
|
|||||||
mz_zip_writer_end(&zip);
|
mz_zip_writer_end(&zip);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const QByteArray bytes(static_cast<const char *>(buffer), qsizetype(size));
|
QByteArray bytes(static_cast<const char *>(buffer), qsizetype(size));
|
||||||
mz_zip_writer_end(&zip); //frees buffer
|
mz_zip_writer_end(&zip); //frees buffer
|
||||||
|
if (!setEntryDates(&bytes)) {
|
||||||
|
setError(error, QStringLiteral("cannot date the zip entries"));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,8 @@
|
|||||||
megabytes, not gigabytes -- and written through QSaveFile, so a failed
|
megabytes, not gigabytes -- and written through QSaveFile, so a failed
|
||||||
save leaves the previous file as it was.
|
save leaves the previous file as it was.
|
||||||
|
|
||||||
Entries carry no timestamp: saving the same project twice gives the
|
Every entry is dated 1 Jan 1980 00:00, not the time of saving: saving
|
||||||
same bytes.
|
the same project twice gives the same bytes.
|
||||||
*/
|
*/
|
||||||
namespace QetZip
|
namespace QetZip
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -49,6 +49,8 @@
|
|||||||
#include <QMetaEnum>
|
#include <QMetaEnum>
|
||||||
#include <QTextDocument>
|
#include <QTextDocument>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QSqlDriver>
|
#include <QSqlDriver>
|
||||||
#include <QSqlError>
|
#include <QSqlError>
|
||||||
@@ -137,7 +139,7 @@ void projectDataBase::updateDB()
|
|||||||
@brief projectDataBase::keepUnbuiltFolioRows
|
@brief projectDataBase::keepUnbuiltFolioRows
|
||||||
Before the tables are rebuilt from the folios: keep the element,
|
Before the tables are rebuilt from the folios: keep the element,
|
||||||
element_info, terminal, conductor and link rows of the folios not built
|
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
|
folios can have changed since, and the rebuild puts them back where
|
||||||
each folio comes (restoreKeptRows()). Nothing to do when every folio
|
each folio comes (restoreKeptRows()). Nothing to do when every folio
|
||||||
is built.
|
is built.
|
||||||
@@ -253,6 +255,58 @@ void projectDataBase::updateDB(const QDomDocument &document)
|
|||||||
updateDB();
|
updateDB();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief projectDataBase::documentRefusal
|
||||||
|
@return why populateFromDocument() will refuse @p document, for the
|
||||||
|
reasons it gives that the document alone can tell -- a folio, symbol
|
||||||
|
or wire with no saved uuid, two symbols sharing one, a folio number
|
||||||
|
using %autonum -- or an empty string. The other reasons need the
|
||||||
|
project's definitions or every folio's links; a document refused for
|
||||||
|
one of those is still filled from the folios, only later.
|
||||||
|
*/
|
||||||
|
QString projectDataBase::documentRefusal(const QDomDocument &document)
|
||||||
|
{
|
||||||
|
if (qEnvironmentVariableIntValue("QET_DATABASE_FROM_FOLIOS") == 1) {
|
||||||
|
return QStringLiteral("QET_DATABASE_FROM_FOLIOS is set");
|
||||||
|
}
|
||||||
|
QSet<QUuid> element_uuids;
|
||||||
|
const QDomNodeList diagram_nodes = document.elementsByTagName(QStringLiteral("diagram"));
|
||||||
|
for (int i = 0 ; i < diagram_nodes.size() ; ++i)
|
||||||
|
{
|
||||||
|
QDomElement diagram_xml = diagram_nodes.at(i).toElement();
|
||||||
|
if (QUuid(diagram_xml.attribute(QStringLiteral("uuid"))).isNull()) {
|
||||||
|
return QStringLiteral("a folio has no saved uuid");
|
||||||
|
}
|
||||||
|
if (diagram_xml.attribute(QStringLiteral("folio")).contains(QStringLiteral("%autonum"))) {
|
||||||
|
return QStringLiteral("a folio number uses %autonum");
|
||||||
|
}
|
||||||
|
for (QDomElement element_xml : QET::findInDomElement(
|
||||||
|
diagram_xml, QStringLiteral("elements"), QStringLiteral("element")))
|
||||||
|
{
|
||||||
|
//Skipped by Diagram::fromXml() as well
|
||||||
|
if (!Element::valideXml(element_xml)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QUuid uuid(element_xml.attribute(QStringLiteral("uuid")));
|
||||||
|
if (uuid.isNull() || element_uuids.contains(uuid)) {
|
||||||
|
return QStringLiteral("an element has no saved uuid, or shares one");
|
||||||
|
}
|
||||||
|
element_uuids.insert(uuid);
|
||||||
|
}
|
||||||
|
for (QDomElement conductor_xml : QET::findInDomElement(
|
||||||
|
diagram_xml, QStringLiteral("conductors"), QStringLiteral("conductor")))
|
||||||
|
{
|
||||||
|
if (Conductor::valideXml(conductor_xml)
|
||||||
|
&& (QUuid(conductor_xml.attribute(QStringLiteral("uuid"))).isNull()
|
||||||
|
|| !conductor_xml.hasAttribute(QStringLiteral("element1"))
|
||||||
|
|| !conductor_xml.hasAttribute(QStringLiteral("element2")))) {
|
||||||
|
return QStringLiteral("a conductor has no saved uuid, or names its ends the older way");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
struct DocumentTerminal
|
struct DocumentTerminal
|
||||||
{
|
{
|
||||||
@@ -305,6 +359,8 @@ struct DocumentElement
|
|||||||
QString type;
|
QString type;
|
||||||
QString sub_type;
|
QString sub_type;
|
||||||
QVariant group;
|
QVariant group;
|
||||||
|
QString location;
|
||||||
|
autonum::sequentialNumbers sequence;
|
||||||
DiagramContext informations;
|
DiagramContext informations;
|
||||||
QString label;
|
QString label;
|
||||||
QHash<QUuid, DocumentTerminal> terminals;
|
QHash<QUuid, DocumentTerminal> terminals;
|
||||||
@@ -325,6 +381,27 @@ bool readSequence(const QDomElement &item, autonum::sequentialNumbers *sequence)
|
|||||||
return true;
|
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
|
struct DocumentConductor
|
||||||
{
|
{
|
||||||
QString uuid;
|
QString uuid;
|
||||||
@@ -376,7 +453,7 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString
|
|||||||
//The folios, in the order the project read them (readDiagramsXml()),
|
//The folios, in the order the project read them (readDiagramsXml()),
|
||||||
//each with its saved uuid -- the one the built folio has.
|
//each with its saved uuid -- the one the built folio has.
|
||||||
const QDomNodeList diagram_nodes = document.elementsByTagName(QStringLiteral("diagram"));
|
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
|
//conductors of those built
|
||||||
const QList<Diagram *> diagrams = m_project->folios();
|
const QList<Diagram *> diagrams = m_project->folios();
|
||||||
if (diagram_nodes.size() != diagrams.size()) {
|
if (diagram_nodes.size() != diagrams.size()) {
|
||||||
@@ -555,6 +632,8 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString
|
|||||||
element.type = known->type;
|
element.type = known->type;
|
||||||
element.sub_type = known->sub_type;
|
element.sub_type = known->sub_type;
|
||||||
element.terminals = known->terminals;
|
element.terminals = known->terminals;
|
||||||
|
element.location = element_xml.attribute(QStringLiteral("type"));
|
||||||
|
element.sequence = elementSequence(element_xml);
|
||||||
const QUuid group = ItemGroups::read(element_xml);
|
const QUuid group = ItemGroups::read(element_xml);
|
||||||
element.group = group.isNull() ? QVariant() : QVariant(group.toString());
|
element.group = group.isNull() ? QVariant() : QVariant(group.toString());
|
||||||
element.informations.fromXml(
|
element.informations.fromXml(
|
||||||
@@ -756,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(":type"), element.type);
|
||||||
m_insert_elements_query.bindValue(QStringLiteral(":sub_type"), element.sub_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(":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()) {
|
if (!m_insert_elements_query.exec()) {
|
||||||
qDebug() << "projectDataBase::populateFromDocument element insert error : " << m_insert_elements_query.lastError();
|
qDebug() << "projectDataBase::populateFromDocument element insert error : " << m_insert_elements_query.lastError();
|
||||||
}
|
}
|
||||||
@@ -829,7 +910,7 @@ QSet<QUuid> projectDataBase::foliosWithUnjoinedWires() const
|
|||||||
@brief projectDataBase::elementTypesOnFolios
|
@brief projectDataBase::elementTypesOnFolios
|
||||||
@return the kind (ElementData::typeToString()) of every symbol on the
|
@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 @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
|
and nothing can have changed them. A symbol whose definition was not
|
||||||
found has no row.
|
found has no row.
|
||||||
*/
|
*/
|
||||||
@@ -850,11 +931,466 @@ QHash<QUuid, QString> projectDataBase::elementTypesOnFolios(const QSet<QUuid> &f
|
|||||||
return types;
|
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
|
@brief projectDataBase::folioUuidsOfElements
|
||||||
@return the uuids of the folios the symbols @p elements are on, read
|
@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
|
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.
|
document and nothing can have changed it.
|
||||||
*/
|
*/
|
||||||
QSet<QUuid> projectDataBase::folioUuidsOfElements(const QSet<QUuid> &elements) const
|
QSet<QUuid> projectDataBase::folioUuidsOfElements(const QSet<QUuid> &elements) const
|
||||||
@@ -992,10 +1528,11 @@ QSqlQuery projectDataBase::newQuery(const QString &query, QString *error) {
|
|||||||
//storeConductorProperties().
|
//storeConductorProperties().
|
||||||
//The drawing tables hold what only built items know (a text's
|
//The drawing tables hold what only built items know (a text's
|
||||||
//laid-out size, a picture's pixels): with folios not built yet
|
//laid-out size, a picture's pixels): with folios not built yet
|
||||||
//(QET_LAZY_FOLIOS) a read of them builds those first. Every other
|
//(lazy folios) a read of them builds those first, and so does a
|
||||||
//table was filled from the document.
|
//read of the view made of them. Every other table was filled from
|
||||||
|
//the document.
|
||||||
static const QRegularExpression drawing_tables(
|
static const QRegularExpression drawing_tables(
|
||||||
QStringLiteral("\\b(shape|independent_text|image)\\b"),
|
QStringLiteral("\\b(shape|independent_text|image|drawing_item_view)\\b"),
|
||||||
QRegularExpression::CaseInsensitiveOption);
|
QRegularExpression::CaseInsensitiveOption);
|
||||||
if (m_project && m_project->unloadedFolioCount() && drawing_tables.match(query).hasMatch()) {
|
if (m_project && m_project->unloadedFolioCount() && drawing_tables.match(query).hasMatch()) {
|
||||||
m_project->loadFolios();
|
m_project->loadFolios();
|
||||||
@@ -1062,8 +1599,10 @@ int projectDataBase::excludedConductorCount() const
|
|||||||
return 0;
|
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;
|
int count = 0;
|
||||||
for (auto *diagram : m_project->diagrams())
|
for (auto *diagram : m_project->builtFolios())
|
||||||
{
|
{
|
||||||
const auto conductor_list = diagram->conductors();
|
const auto conductor_list = diagram->conductors();
|
||||||
for (auto *conductor : conductor_list)
|
for (auto *conductor : conductor_list)
|
||||||
@@ -1248,11 +1787,12 @@ void projectDataBase::flushElementPositions()
|
|||||||
/**
|
/**
|
||||||
@brief projectDataBase::updateFolioPositions
|
@brief projectDataBase::updateFolioPositions
|
||||||
Write every folio's position and folio number again, after a folio was
|
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()
|
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(":pos", m_project->folioIndex(diagram)+1);
|
||||||
m_diagram_order_changed.bindValue(":uuid", diagram->uuid().toString());
|
m_diagram_order_changed.bindValue(":uuid", diagram->uuid().toString());
|
||||||
@@ -1381,6 +1921,13 @@ void projectDataBase::removeDiagram(Diagram *diagram)
|
|||||||
void projectDataBase::diagramInfoChanged(Diagram *diagram)
|
void projectDataBase::diagramInfoChanged(Diagram *diagram)
|
||||||
{
|
{
|
||||||
m_content_changed = true;
|
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);
|
bindDiagramInfoValues(m_update_diagram_info_query, diagram);
|
||||||
|
|
||||||
if (!m_update_diagram_info_query.exec()) {
|
if (!m_update_diagram_info_query.exec()) {
|
||||||
@@ -1912,7 +2459,7 @@ void projectDataBase::populateDrawingItemTables()
|
|||||||
//Queued directly, not through addDrawingItem(): every one of them
|
//Queued directly, not through addDrawingItem(): every one of them
|
||||||
//came onto its folio through Diagram::addItem(), which connected it
|
//came onto its folio through Diagram::addItem(), which connected it
|
||||||
//already, and a full rebuild runs on every load. A folio not built
|
//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.
|
//is built.
|
||||||
for (auto diagram : m_project->folios())
|
for (auto diagram : m_project->folios())
|
||||||
{
|
{
|
||||||
@@ -1965,6 +2512,8 @@ bool projectDataBase::createDataBase()
|
|||||||
"type VARCHAR(50),"
|
"type VARCHAR(50),"
|
||||||
"sub_type VARCHAR(50),"
|
"sub_type VARCHAR(50),"
|
||||||
"group_uuid VARCHAR(50),"
|
"group_uuid VARCHAR(50),"
|
||||||
|
"definition TEXT,"
|
||||||
|
"sequence TEXT,"
|
||||||
"FOREIGN KEY (diagram_uuid) REFERENCES diagram (uuid)"
|
"FOREIGN KEY (diagram_uuid) REFERENCES diagram (uuid)"
|
||||||
")");
|
")");
|
||||||
if (!query_.exec(element_table)) {
|
if (!query_.exec(element_table)) {
|
||||||
@@ -2445,7 +2994,7 @@ void projectDataBase::populateElementTable()
|
|||||||
/**
|
/**
|
||||||
@brief projectDataBase::populateElementInfoTable
|
@brief projectDataBase::populateElementInfoTable
|
||||||
Populate the element info table from the symbol information store
|
Populate the element info table from the symbol information store
|
||||||
(DB-ACCESSORS-PLAN.md stage 4.3), in the order of the element table.
|
in the order of the element table.
|
||||||
The label column is the label a symbol shows, which its folio's
|
The label column is the label a symbol shows, which its folio's
|
||||||
numbering decides: the symbol holding the uuid gives it.
|
numbering decides: the symbol holding the uuid gives it.
|
||||||
|
|
||||||
@@ -3214,7 +3763,7 @@ void projectDataBase::prefillFromDocument(const QDomDocument &document)
|
|||||||
@brief projectDataBase::endPrefill
|
@brief projectDataBase::endPrefill
|
||||||
The folios are built: compare what prefillFromDocument() stored with
|
The folios are built: compare what prefillFromDocument() stored with
|
||||||
what building stored, and forget what nothing was placed for -- unless
|
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.
|
the store then answers for their items until they are.
|
||||||
*/
|
*/
|
||||||
void projectDataBase::endPrefill(bool keep_unplaced)
|
void projectDataBase::endPrefill(bool keep_unplaced)
|
||||||
@@ -3426,7 +3975,7 @@ void projectDataBase::flushLinks()
|
|||||||
qDebug() << "projectDataBase::flushLinks insert error : " << insert.lastError();
|
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
|
//be linked to once that folio is
|
||||||
for (const auto &waiting : element->waitingLinks())
|
for (const auto &waiting : element->waitingLinks())
|
||||||
{
|
{
|
||||||
@@ -3628,7 +4177,8 @@ void projectDataBase::prepareQuery()
|
|||||||
m_diagram_info_order_changed.prepare("UPDATE diagram_info SET folio = :folio WHERE diagram_uuid = :uuid");
|
m_diagram_info_order_changed.prepare("UPDATE diagram_info SET folio = :folio WHERE diagram_uuid = :uuid");
|
||||||
|
|
||||||
//INSERT ELEMENT
|
//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 = QSqlQuery(m_data_base);
|
||||||
m_insert_elements_query.prepare(insert_element_query);
|
m_insert_elements_query.prepare(insert_element_query);
|
||||||
|
|
||||||
@@ -3743,6 +4293,8 @@ void projectDataBase::bindElementValues(QSqlQuery &query, Element *element, Diag
|
|||||||
query.bindValue(QStringLiteral(":type"), element_data.typeToString());
|
query.bindValue(QStringLiteral(":type"), element_data.typeToString());
|
||||||
query.bindValue(QStringLiteral(":sub_type"), element_data.masterTypeToString());
|
query.bindValue(QStringLiteral(":sub_type"), element_data.masterTypeToString());
|
||||||
query.bindValue(QStringLiteral(":group_uuid"), groupValue(element));
|
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 <QSet>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
#include "../diagramcontext.h"
|
#include "../diagramcontext.h"
|
||||||
#include "../titleblockproperties.h"
|
#include "../titleblockproperties.h"
|
||||||
#include "../conductorproperties.h"
|
#include "../conductorproperties.h"
|
||||||
|
#include "../autoNum/assignvariables.h"
|
||||||
|
|
||||||
class Element;
|
class Element;
|
||||||
class QETProject;
|
class QETProject;
|
||||||
@@ -67,6 +70,14 @@ class projectDataBase : public QObject
|
|||||||
populateFromDocument()), from the built folios otherwise.
|
populateFromDocument()), from the built folios otherwise.
|
||||||
*/
|
*/
|
||||||
void updateDB(const QDomDocument &document);
|
void updateDB(const QDomDocument &document);
|
||||||
|
/**
|
||||||
|
Why updateDB(document) will fill the tables from the folios
|
||||||
|
rather than from @p document, for the reasons the document
|
||||||
|
alone can tell; empty when it knows of none. Read before the
|
||||||
|
folios are: a project whose tables will come from the folios
|
||||||
|
builds every folio as it opens, even with QET_LAZY_FOLIOS.
|
||||||
|
*/
|
||||||
|
static QString documentRefusal(const QDomDocument &document);
|
||||||
/**
|
/**
|
||||||
Suppress the full rebuild performed by updateDB().
|
Suppress the full rebuild performed by updateDB().
|
||||||
|
|
||||||
@@ -106,7 +117,7 @@ class projectDataBase : public QObject
|
|||||||
|
|
||||||
//The information of every symbol placed in the project, by
|
//The information of every symbol placed in the project, by
|
||||||
//symbol uuid: the store each symbol's information is written
|
//symbol uuid: the store each symbol's information is written
|
||||||
//through (DB-ACCESSORS-PLAN.md stage 4.1). A symbol not placed
|
//through. A symbol not placed
|
||||||
//(a preview, the symbol editor) keeps its own.
|
//(a preview, the symbol editor) keeps its own.
|
||||||
DiagramContext elementInformation(const QUuid &element) const;
|
DiagramContext elementInformation(const QUuid &element) const;
|
||||||
bool hasElementInformation(const QUuid &element) const;
|
bool hasElementInformation(const QUuid &element) const;
|
||||||
@@ -124,7 +135,7 @@ class projectDataBase : public QObject
|
|||||||
|
|
||||||
//The title block properties of every folio of the project, by
|
//The title block properties of every folio of the project, by
|
||||||
//folio uuid: the store each folio's title block is written
|
//folio uuid: the store each folio's title block is written
|
||||||
//through (DB-ACCESSORS-PLAN.md stage 4.4). A folio not in a
|
//through. A folio not in a
|
||||||
//project keeps its own.
|
//project keeps its own.
|
||||||
TitleBlockProperties folioTitleBlock(const QUuid &folio) const;
|
TitleBlockProperties folioTitleBlock(const QUuid &folio) const;
|
||||||
const TitleBlockProperties *storedFolioTitleBlock(const QUuid &folio) const;
|
const TitleBlockProperties *storedFolioTitleBlock(const QUuid &folio) const;
|
||||||
@@ -136,7 +147,7 @@ class projectDataBase : public QObject
|
|||||||
|
|
||||||
//The properties of every wire placed in the project, by wire
|
//The properties of every wire placed in the project, by wire
|
||||||
//uuid: the store each wire's properties are written through
|
//uuid: the store each wire's properties are written through
|
||||||
//(DB-ACCESSORS-PLAN.md stage 4.5). A wire not placed keeps its own.
|
//A wire not placed keeps its own.
|
||||||
ConductorProperties conductorProperties(const QUuid &conductor) const;
|
ConductorProperties conductorProperties(const QUuid &conductor) const;
|
||||||
bool hasConductorProperties(const QUuid &conductor) const;
|
bool hasConductorProperties(const QUuid &conductor) const;
|
||||||
int placedConductorCount(const QUuid &conductor) const;
|
int placedConductorCount(const QUuid &conductor) const;
|
||||||
@@ -145,11 +156,11 @@ class projectDataBase : public QObject
|
|||||||
QStringList conductorPropertiesMismatches() const;
|
QStringList conductorPropertiesMismatches() const;
|
||||||
|
|
||||||
//Fill the three stores from a project's document before its
|
//Fill the three stores from a project's document before its
|
||||||
//folios are built (DB-ACCESSORS-PLAN.md stage 4.6), then, once
|
//folios are built, then, once
|
||||||
//they are, say how many of those values building agreed with.
|
//they are, say how many of those values building agreed with.
|
||||||
void prefillFromDocument(const QDomDocument &document);
|
void prefillFromDocument(const QDomDocument &document);
|
||||||
void endPrefill(bool keep_unplaced = false);
|
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
|
//its rows came from the document already, so none are written
|
||||||
//and the database is not marked changed
|
//and the database is not marked changed
|
||||||
void setBuildingFolio(bool building);
|
void setBuildingFolio(bool building);
|
||||||
@@ -158,7 +169,37 @@ class projectDataBase : public QObject
|
|||||||
QString prefillReport() const;
|
QString prefillReport() const;
|
||||||
QSet<QUuid> foliosWithUnjoinedWires() const;
|
QSet<QUuid> foliosWithUnjoinedWires() const;
|
||||||
QSet<QUuid> folioUuidsOfElements(const QSet<QUuid> &elements) 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;
|
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 addElement (Element *element);
|
||||||
void removeElement (Element *element);
|
void removeElement (Element *element);
|
||||||
@@ -295,6 +336,14 @@ class projectDataBase : public QObject
|
|||||||
//briefly gives a copy its original's uuid, so one uuid can have
|
//briefly gives a copy its original's uuid, so one uuid can have
|
||||||
//two symbols until the copy's is renewed.
|
//two symbols until the copy's is renewed.
|
||||||
QMultiHash<QUuid, QPointer<Element>> m_placed_elements;
|
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, TitleBlockProperties> m_folio_titleblocks;
|
||||||
QHash<QUuid, QPointer<Diagram>> m_placed_folios;
|
QHash<QUuid, QPointer<Diagram>> m_placed_folios;
|
||||||
QHash<QUuid, ConductorProperties> m_conductor_properties;
|
QHash<QUuid, ConductorProperties> m_conductor_properties;
|
||||||
@@ -306,7 +355,7 @@ class projectDataBase : public QObject
|
|||||||
//The folios whose rows a rebuild keeps (keepUnbuiltFolioRows())
|
//The folios whose rows a rebuild keeps (keepUnbuiltFolioRows())
|
||||||
QSet<Diagram *> m_kept_folios;
|
QSet<Diagram *> m_kept_folios;
|
||||||
//The tables' rows were filled from the document: those of a
|
//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;
|
bool m_rows_from_document = false;
|
||||||
void keepUnbuiltFolioRows();
|
void keepUnbuiltFolioRows();
|
||||||
void restoreKeptRows(const QString &table, Diagram *folio, const QString &where);
|
void restoreKeptRows(const QString &table, Diagram *folio, const QString &where);
|
||||||
|
|||||||
+213
-63
@@ -49,9 +49,12 @@
|
|||||||
#include "diagramsortkeys.h"
|
#include "diagramsortkeys.h"
|
||||||
#include "itemgroups.h"
|
#include "itemgroups.h"
|
||||||
#include "textgrid.h"
|
#include "textgrid.h"
|
||||||
|
#include "container/qetcontainer.h"
|
||||||
#include "foliogrid.h"
|
#include "foliogrid.h"
|
||||||
#include <QGraphicsView>
|
#include <QGraphicsView>
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
|
#include <QThreadPool>
|
||||||
|
#include <QtConcurrentMap>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <climits>
|
#include <climits>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
@@ -772,13 +775,13 @@ void Diagram::keyReleaseEvent(QKeyEvent *e)
|
|||||||
/**
|
/**
|
||||||
@brief Diagram::ensureLoaded
|
@brief Diagram::ensureLoaded
|
||||||
Build this folio's items from the XML kept when the project was
|
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
|
every folio's (QETProject::folioLoaded()). Nothing for a folio built
|
||||||
already.
|
already.
|
||||||
*/
|
*/
|
||||||
void Diagram::ensureLoaded()
|
void Diagram::ensureLoaded()
|
||||||
{
|
{
|
||||||
if (m_deferred.isNull()) {
|
if (isLoaded()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (m_project) {
|
if (m_project) {
|
||||||
@@ -788,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
|
@brief Diagram::buildDeferred
|
||||||
Build this folio's items from the XML kept for later, and nothing else.
|
Build this folio's items from the XML kept for later, and nothing else.
|
||||||
@@ -797,17 +893,21 @@ void Diagram::ensureLoaded()
|
|||||||
*/
|
*/
|
||||||
bool Diagram::buildDeferred()
|
bool Diagram::buildDeferred()
|
||||||
{
|
{
|
||||||
if (m_deferred.isNull()) {
|
if (isLoaded()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
QDomElement xml = m_deferred;
|
QDomElement xml = m_deferred;
|
||||||
m_deferred = QDomElement();
|
m_deferred = QDomElement();
|
||||||
|
m_kept_part.clear();
|
||||||
m_kept_text.clear();
|
m_kept_text.clear();
|
||||||
projectDataBase *db = m_project ? m_project->dataBase() : nullptr;
|
projectDataBase *db = m_project ? m_project->dataBase() : nullptr;
|
||||||
if (db) db->setBuildingFolio(true);
|
if (db) db->setBuildingFolio(true);
|
||||||
m_loading_deferred = true;
|
m_loading_deferred = true;
|
||||||
fromXml(xml, QPointF(), true, nullptr);
|
fromXml(xml, QPointF(), true, nullptr);
|
||||||
m_loading_deferred = false;
|
m_loading_deferred = false;
|
||||||
|
m_partial_elements.clear();
|
||||||
|
m_partial_table.clear();
|
||||||
|
m_partial_changed = false;
|
||||||
if (db) db->setBuildingFolio(false);
|
if (db) db->setBuildingFolio(false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1133,7 +1233,7 @@ QList < QSet <Conductor *> > Diagram::potentials()
|
|||||||
\~French Un Document XML (QDomDocument)
|
\~French Un Document XML (QDomDocument)
|
||||||
*/
|
*/
|
||||||
QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) {
|
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
|
//its built items (QETProject::toXml() may write one not built as
|
||||||
//it was read instead)
|
//it was read instead)
|
||||||
ensureLoaded();
|
ensureLoaded();
|
||||||
@@ -1246,7 +1346,7 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) {
|
|||||||
//Wires that tie on their ends (common: several from one terminal)
|
//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
|
//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
|
//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(),
|
std::stable_sort(list_conductors.begin(), list_conductors.end(),
|
||||||
[](Conductor *a, Conductor *b) {
|
[](Conductor *a, Conductor *b) {
|
||||||
const QString ka = conductorSortKey(a), kb = conductorSortKey(b);
|
const QString ka = conductorSortKey(a), kb = conductorSortKey(b);
|
||||||
@@ -1486,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
|
@brief Diagram::fromXml
|
||||||
Imports the described schema into an XML document.
|
Imports the described schema into an XML document.
|
||||||
@@ -1861,7 +2028,7 @@ bool Diagram::fromXml(QDomElement &document,
|
|||||||
return(true);
|
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
|
//they are first needed: until then the folio has what it says
|
||||||
//about itself (uuid, title block, border, numbering) and keeps its
|
//about itself (uuid, title block, border, numbering) and keeps its
|
||||||
//XML, which belongs to the project's document.
|
//XML, which belongs to the project's document.
|
||||||
@@ -1907,65 +2074,23 @@ bool Diagram::fromXml(QDomElement &document,
|
|||||||
//Load all elements from the XML
|
//Load all elements from the XML
|
||||||
QList<Element *> added_elements;
|
QList<Element *> added_elements;
|
||||||
QHash<int, Terminal *> table_adr_id;
|
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 :
|
for (auto element_xml :
|
||||||
QET::findInDomElement(root, QStringLiteral("elements"), QStringLiteral("element")))
|
QET::findInDomElement(root, QStringLiteral("elements"), QStringLiteral("element")))
|
||||||
{
|
{
|
||||||
if (!Element::valideXml(element_xml)) continue;
|
if (!Element::valideXml(element_xml)) continue;
|
||||||
|
//Built already as another folio's partner (buildSymbols())
|
||||||
// cree un element dont le type correspond a l'id type
|
if (m_loading_deferred) {
|
||||||
QString type_id = element_xml.attribute(QStringLiteral("type"));
|
Element *built = m_partial_elements.value(QUuid(element_xml.attribute(QStringLiteral("uuid"))));
|
||||||
ElementsLocation element_location;
|
if (built) {
|
||||||
if (type_id.startsWith(QStringLiteral("embed://"))) {
|
added_elements << built;
|
||||||
element_location = ElementsLocation(type_id, m_project);
|
continue;
|
||||||
}
|
|
||||||
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'))));
|
|
||||||
}
|
}
|
||||||
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
|
//Texts, images and shapes written before they carried a uuid (or
|
||||||
@@ -2005,12 +2130,37 @@ bool Diagram::fromXml(QDomElement &document,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load image
|
// Load image
|
||||||
|
// Decoding is most of the time of a folio with photos (a quarter of
|
||||||
|
// a second each), so the pictures are decoded first, in parallel,
|
||||||
|
// and the items built afterwards in this thread.
|
||||||
|
const QList<QDomElement> images_xml = QET::findInDomElement(
|
||||||
|
root, QStringLiteral("images"), QStringLiteral("image"));
|
||||||
|
QList<DiagramImageItem::Picture> pictures;
|
||||||
|
pictures.reserve(images_xml.size());
|
||||||
|
for (const QDomElement &image_xml : images_xml) {
|
||||||
|
const QString picture_file = QetContainer::pictureFile(image_xml);
|
||||||
|
pictures << DiagramImageItem::pictureOf(
|
||||||
|
image_xml,
|
||||||
|
!picture_file.isEmpty() && project()
|
||||||
|
? project() -> containerPicture(picture_file)
|
||||||
|
: QByteArray());
|
||||||
|
}
|
||||||
|
if (pictures.size() > 1) {
|
||||||
|
QThreadPool pool;
|
||||||
|
pool.setMaxThreadCount(std::min<int>(QThread::idealThreadCount(),
|
||||||
|
pictures.size()));
|
||||||
|
QtConcurrent::blockingMap(&pool, pictures,
|
||||||
|
&DiagramImageItem::decodePicture);
|
||||||
|
} else {
|
||||||
|
for (DiagramImageItem::Picture &picture : pictures)
|
||||||
|
DiagramImageItem::decodePicture(picture);
|
||||||
|
}
|
||||||
|
|
||||||
QList<DiagramImageItem *> added_images;
|
QList<DiagramImageItem *> added_images;
|
||||||
for (auto image_xml : QET::findInDomElement(root,
|
for (qsizetype i = 0 ; i < images_xml.size() ; ++i) {
|
||||||
QStringLiteral("images"),
|
const QDomElement &image_xml = images_xml.at(i);
|
||||||
QStringLiteral("image"))) {
|
|
||||||
DiagramImageItem *dii = new DiagramImageItem ();
|
DiagramImageItem *dii = new DiagramImageItem ();
|
||||||
dii -> fromXml(image_xml);
|
dii -> fromXml(image_xml, std::move(pictures[i]));
|
||||||
settle_uuid(dii, image_xml, QStringLiteral("image"), added_images.size());
|
settle_uuid(dii, image_xml, QStringLiteral("image"), added_images.size());
|
||||||
ItemGroups::setGroup(dii, ItemGroups::read(image_xml));
|
ItemGroups::setGroup(dii, ItemGroups::read(image_xml));
|
||||||
addItem(dii);
|
addItem(dii);
|
||||||
|
|||||||
+26
-1
@@ -133,6 +133,7 @@ class Diagram : public QGraphicsScene
|
|||||||
bool draw_terminals_;
|
bool draw_terminals_;
|
||||||
bool draw_terminal_names_;
|
bool draw_terminal_names_;
|
||||||
bool draw_colored_conductors_;
|
bool draw_colored_conductors_;
|
||||||
|
bool m_small_pictures = false;
|
||||||
|
|
||||||
QString m_conductors_autonum_name;
|
QString m_conductors_autonum_name;
|
||||||
DiagramEventInterface *m_event_interface;
|
DiagramEventInterface *m_event_interface;
|
||||||
@@ -142,6 +143,12 @@ class Diagram : public QGraphicsScene
|
|||||||
QUuid m_uuid = QUuid::createUuid();
|
QUuid m_uuid = QUuid::createUuid();
|
||||||
/// The XML of this folio's items, while they are not built yet
|
/// The XML of this folio's items, while they are not built yet
|
||||||
QDomElement m_deferred;
|
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;
|
QString m_opened_header;
|
||||||
QByteArray m_kept_text;
|
QByteArray m_kept_text;
|
||||||
bool m_loading_deferred = false;
|
bool m_loading_deferred = false;
|
||||||
@@ -211,9 +218,20 @@ class Diagram : public QGraphicsScene
|
|||||||
bool = true,
|
bool = true,
|
||||||
DiagramContent * = nullptr);
|
DiagramContent * = nullptr);
|
||||||
/// false while this folio's items wait in its XML to be
|
/// false while this folio's items wait in its XML to be
|
||||||
/// built (QET_LAZY_FOLIOS, LAZY-FOLIO-PLAN.md)
|
/// built (lazy folios)
|
||||||
bool isLoaded() const {return m_deferred.isNull();}
|
bool isLoaded() const {return m_deferred.isNull();}
|
||||||
QDomElement deferredXml() const {return m_deferred;}
|
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();
|
QString headerXml();
|
||||||
/// The folio's text in the file it was read from, while it is
|
/// The folio's text in the file it was read from, while it is
|
||||||
/// not built (QETProject::toXml())
|
/// not built (QETProject::toXml())
|
||||||
@@ -223,6 +241,8 @@ class Diagram : public QGraphicsScene
|
|||||||
/// headerChanged() (QETProject::toXml())
|
/// headerChanged() (QETProject::toXml())
|
||||||
void rememberHeader() {m_opened_header = headerXml();}
|
void rememberHeader() {m_opened_header = headerXml();}
|
||||||
bool headerChanged() {return headerXml() != m_opened_header;}
|
bool headerChanged() {return headerXml() != m_opened_header;}
|
||||||
|
bool headerChangeShowsOnly();
|
||||||
|
QDomElement deferredXmlWithHeader(QDomDocument &document);
|
||||||
void ensureLoaded();
|
void ensureLoaded();
|
||||||
bool buildDeferred();
|
bool buildDeferred();
|
||||||
//The last argument is only for a copy fragment: when it is
|
//The last argument is only for a copy fragment: when it is
|
||||||
@@ -284,6 +304,11 @@ class Diagram : public QGraphicsScene
|
|||||||
void setDrawTerminalNames(bool);
|
void setDrawTerminalNames(bool);
|
||||||
bool drawColoredConductors() const;
|
bool drawColoredConductors() const;
|
||||||
void setDrawColoredConductors(bool);
|
void setDrawColoredConductors(bool);
|
||||||
|
/// While true, pictures draw a copy of a few times the size
|
||||||
|
/// they are drawn at, not their full resolution (SVG export,
|
||||||
|
/// ExportProperties::svg_small_pictures)
|
||||||
|
bool smallPictures() const {return m_small_pictures;}
|
||||||
|
void setSmallPictures(bool small) {m_small_pictures = small;}
|
||||||
|
|
||||||
QString title() const;
|
QString title() const;
|
||||||
bool toPaintDevice(QPaintDevice &, int = -1, int = -1,
|
bool toPaintDevice(QPaintDevice &, int = -1, int = -1,
|
||||||
|
|||||||
+29
-10
@@ -79,13 +79,7 @@ DiagramContent::DiagramContent(Diagram *diagram, bool selected) :
|
|||||||
m_other_conductors << c;
|
m_other_conductors << c;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_potential_conductors.isEmpty()) {
|
m_potential_candidates << c;
|
||||||
m_potential_conductors << c;
|
|
||||||
} else {
|
|
||||||
if (!potentialIsManaged(c->relatedPotentialConductors(true).values())) {
|
|
||||||
m_potential_conductors << c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DiagramImageItem::Type: { m_images << qgraphicsitem_cast<DiagramImageItem *>(item); 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))
|
if(!m_selected_items.contains(qgi))
|
||||||
m_selected_items << 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();
|
QList<Conductor *> c_list = c->relatedPotentialConductors(true).values();
|
||||||
c_list << c;
|
c_list << c;
|
||||||
if (!potentialIsManaged(c_list)) {
|
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)
|
bool DiagramContent::potentialIsManaged(QList<Conductor *> conductors)
|
||||||
{
|
{
|
||||||
|
const QList<Conductor *> potentials = potentialConductors();
|
||||||
bool b = false;
|
bool b = false;
|
||||||
|
|
||||||
for (Conductor *c : conductors)
|
for (Conductor *c : conductors)
|
||||||
{
|
{
|
||||||
if (m_potential_conductors.contains(c))
|
if (potentials.contains(c))
|
||||||
b = true;
|
b = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return b;
|
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
|
@brief DiagramContent::hasTextEditing
|
||||||
@return true if handle a text currently in editing interaction
|
@return true if handle a text currently in editing interaction
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#define DIAGRAM_CONTENT_H
|
#define DIAGRAM_CONTENT_H
|
||||||
|
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
|
#include <optional>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
|
|
||||||
#include "../qetgraphicsitem/terminalelement.h"
|
#include "../qetgraphicsitem/terminalelement.h"
|
||||||
@@ -77,7 +78,6 @@ class DiagramContent
|
|||||||
QList<Conductor *> m_conductors_to_update;
|
QList<Conductor *> m_conductors_to_update;
|
||||||
QList<Conductor *> m_conductors_to_move;
|
QList<Conductor *> m_conductors_to_move;
|
||||||
QList<Conductor *> m_other_conductors;
|
QList<Conductor *> m_other_conductors;
|
||||||
QList<Conductor *> m_potential_conductors;
|
|
||||||
QSet<DynamicElementTextItem *> m_element_texts;
|
QSet<DynamicElementTextItem *> m_element_texts;
|
||||||
QSet<ElementTextItemGroup *> m_texts_groups;
|
QSet<ElementTextItemGroup *> m_texts_groups;
|
||||||
QList<QGraphicsItem *> m_selected_items;
|
QList<QGraphicsItem *> m_selected_items;
|
||||||
@@ -89,6 +89,7 @@ class DiagramContent
|
|||||||
QList<DiagramTextItem *> selectedTexts() const;
|
QList<DiagramTextItem *> selectedTexts() const;
|
||||||
QList<ElementTextItemGroup *> selectedTextsGroup() const;
|
QList<ElementTextItemGroup *> selectedTextsGroup() const;
|
||||||
QList<Conductor *> conductors(int = AnyConductor) const;
|
QList<Conductor *> conductors(int = AnyConductor) const;
|
||||||
|
QList<Conductor *> potentialConductors() const;
|
||||||
bool hasDeletableItems() const;
|
bool hasDeletableItems() const;
|
||||||
bool hasCopiableItems() const;
|
bool hasCopiableItems() const;
|
||||||
QList<QGraphicsItem *> items(int = All) const;
|
QList<QGraphicsItem *> items(int = All) const;
|
||||||
@@ -103,6 +104,12 @@ class DiagramContent
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
int removePinnedGroups();
|
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 &);
|
QDebug &operator<<(QDebug, DiagramContent &);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1649,7 +1649,7 @@ void DiagramView::editDiagramProperties()
|
|||||||
@brief DiagramView::buildFolio
|
@brief DiagramView::buildFolio
|
||||||
Build the items of the folio this view shows, and those of the folios
|
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
|
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()
|
void DiagramView::buildFolio()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -255,7 +255,7 @@
|
|||||||
<string>margin: 5px; font-weight: bold;</string>
|
<string>margin: 5px; font-weight: bold;</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Potential separation</string>
|
<string>Function separation</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ ElementProvider::ElementProvider(const QList<Diagram *> &diagrams) :
|
|||||||
@brief ElementProvider::folios
|
@brief ElementProvider::folios
|
||||||
@return the folios to search for symbols of the kinds @p kinds (free
|
@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
|
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()).
|
(QETProject::buildFoliosHolding()).
|
||||||
*/
|
*/
|
||||||
QList<Diagram *> ElementProvider::folios(ElementData::Types kinds, bool free_only) const
|
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;
|
QList<Element *> elmt_list;
|
||||||
const QList<Diagram *> searched = folios(filter, true);
|
const QList<Diagram *> searched = folios(filter, true);
|
||||||
//A symbol whose saved partner is on a folio not built yet
|
//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()
|
const QSet<QUuid> waiting = m_project ? m_project->symbolsOnUnbuiltFolios()
|
||||||
: QSet<QUuid>();
|
: 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())
|
for (auto item_ : d->items())
|
||||||
{
|
{
|
||||||
if(item_->type() == QetGraphicsTableItem::Type)
|
if(item_->type() == QetGraphicsTableItem::Type)
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class ElementProvider
|
|||||||
|
|
||||||
QList <Diagram *> m_diagram_list;
|
QList <Diagram *> m_diagram_list;
|
||||||
//Searching a project: its folios are asked for when searched,
|
//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
|
//those a search can find something on
|
||||||
QETProject *m_project = nullptr;
|
QETProject *m_project = nullptr;
|
||||||
Diagram *m_excluded = nullptr;
|
Diagram *m_excluded = nullptr;
|
||||||
|
|||||||
@@ -417,12 +417,14 @@ void ExportDialog::generateSvg(
|
|||||||
|
|
||||||
// genere une QPicture a partir du schema
|
// genere une QPicture a partir du schema
|
||||||
QPicture picture;
|
QPicture picture;
|
||||||
|
diagram -> setSmallPictures(epw -> exportProperties().svg_small_pictures);
|
||||||
diagram -> toPaintDevice(
|
diagram -> toPaintDevice(
|
||||||
picture,
|
picture,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
keep_aspect_ratio ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio
|
keep_aspect_ratio ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio
|
||||||
);
|
);
|
||||||
|
diagram -> setSmallPictures(false);
|
||||||
|
|
||||||
// "joue" la QPicture sur un QSvgGenerator
|
// "joue" la QPicture sur un QSvgGenerator
|
||||||
QSvgGenerator svg_engine;
|
QSvgGenerator svg_engine;
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ ExportProperties::ExportProperties() :
|
|||||||
draw_colored_conductors(true),
|
draw_colored_conductors(true),
|
||||||
dxf_blocks(false),
|
dxf_blocks(false),
|
||||||
dxf_attributes(false),
|
dxf_attributes(false),
|
||||||
|
svg_small_pictures(false),
|
||||||
exported_area(QET::BorderArea)
|
exported_area(QET::BorderArea)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -83,6 +84,8 @@ void ExportProperties::toSettings(QSettings &settings,
|
|||||||
dxf_blocks);
|
dxf_blocks);
|
||||||
settings.setValue(prefix % "dxfattributes",
|
settings.setValue(prefix % "dxfattributes",
|
||||||
dxf_attributes);
|
dxf_attributes);
|
||||||
|
settings.setValue(prefix % "svgsmallpictures",
|
||||||
|
svg_small_pictures);
|
||||||
settings.setValue(prefix % "area",
|
settings.setValue(prefix % "area",
|
||||||
QET::diagramAreaToString(exported_area));
|
QET::diagramAreaToString(exported_area));
|
||||||
}
|
}
|
||||||
@@ -125,6 +128,8 @@ void ExportProperties::fromSettings(QSettings &settings,
|
|||||||
false).toBool();
|
false).toBool();
|
||||||
dxf_attributes = settings.value(prefix % "dxfattributes",
|
dxf_attributes = settings.value(prefix % "dxfattributes",
|
||||||
false).toBool();
|
false).toBool();
|
||||||
|
svg_small_pictures = settings.value(prefix % "svgsmallpictures",
|
||||||
|
false).toBool();
|
||||||
|
|
||||||
exported_area = QET::diagramAreaFromString(
|
exported_area = QET::diagramAreaFromString(
|
||||||
settings.value(
|
settings.value(
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class ExportProperties {
|
|||||||
bool draw_colored_conductors; ///< Whether to render conductors colors
|
bool draw_colored_conductors; ///< Whether to render conductors colors
|
||||||
bool dxf_blocks; ///< DXF: each symbol as a block, placed with an INSERT
|
bool dxf_blocks; ///< DXF: each symbol as a block, placed with an INSERT
|
||||||
bool dxf_attributes; ///< DXF, with dxf_blocks: a symbol's texts as attributes of its INSERT
|
bool dxf_attributes; ///< DXF, with dxf_blocks: a symbol's texts as attributes of its INSERT
|
||||||
|
bool svg_small_pictures; ///< SVG: pictures at most 4x the size they are drawn, not full resolution
|
||||||
QET::DiagramArea exported_area; ///< Area of diagrams to be rendered
|
QET::DiagramArea exported_area; ///< Area of diagrams to be rendered
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ ExportProperties ExportPropertiesWidget::exportProperties() const
|
|||||||
export_properties.draw_colored_conductors = draw_colored_conductors -> isChecked();
|
export_properties.draw_colored_conductors = draw_colored_conductors -> isChecked();
|
||||||
export_properties.dxf_blocks = dxf_blocks -> isChecked();
|
export_properties.dxf_blocks = dxf_blocks -> isChecked();
|
||||||
export_properties.dxf_attributes = dxf_attributes -> isChecked();
|
export_properties.dxf_attributes = dxf_attributes -> isChecked();
|
||||||
|
export_properties.svg_small_pictures = svg_small_pictures -> isChecked();
|
||||||
export_properties.exported_area = export_border -> isChecked() ? QET::BorderArea : QET::ElementsArea;
|
export_properties.exported_area = export_border -> isChecked() ? QET::BorderArea : QET::ElementsArea;
|
||||||
|
|
||||||
return(export_properties);
|
return(export_properties);
|
||||||
@@ -94,6 +95,7 @@ void ExportPropertiesWidget::setExportProperties(const ExportProperties &export_
|
|||||||
dxf_blocks -> setChecked(export_properties.dxf_blocks);
|
dxf_blocks -> setChecked(export_properties.dxf_blocks);
|
||||||
dxf_attributes -> setChecked(export_properties.dxf_attributes);
|
dxf_attributes -> setChecked(export_properties.dxf_attributes);
|
||||||
dxf_attributes -> setEnabled(export_properties.dxf_blocks);
|
dxf_attributes -> setEnabled(export_properties.dxf_blocks);
|
||||||
|
svg_small_pictures -> setChecked(export_properties.svg_small_pictures);
|
||||||
|
|
||||||
if (export_properties.exported_area == QET::BorderArea) {
|
if (export_properties.exported_area == QET::BorderArea) {
|
||||||
export_border -> setChecked(true);
|
export_border -> setChecked(true);
|
||||||
@@ -125,6 +127,7 @@ void ExportPropertiesWidget::setPrintingMode(bool mode) {
|
|||||||
export_border -> setVisible(!mode);
|
export_border -> setVisible(!mode);
|
||||||
export_elements -> setVisible(!mode);
|
export_elements -> setVisible(!mode);
|
||||||
draw_bg_transparent -> setVisible(!mode);
|
draw_bg_transparent -> setVisible(!mode);
|
||||||
|
svg_small_pictures -> setVisible(!mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -236,6 +239,11 @@ void ExportPropertiesWidget::build()
|
|||||||
optionshlayout -> addWidget(dxf_attributes, 5, 1);
|
optionshlayout -> addWidget(dxf_attributes, 5, 1);
|
||||||
connect(dxf_blocks, &QCheckBox::toggled, dxf_attributes, &QCheckBox::setEnabled);
|
connect(dxf_blocks, &QCheckBox::toggled, dxf_attributes, &QCheckBox::setEnabled);
|
||||||
|
|
||||||
|
// pictures not at full resolution in an SVG: a much smaller file
|
||||||
|
svg_small_pictures = new QCheckBox(tr("SVG: smaller pictures"), groupbox_options);
|
||||||
|
svg_small_pictures -> setToolTip(tr("Pictures are stored at four times the size they have on the folio instead of their full resolution: a much smaller file, still sharp when printed"));
|
||||||
|
optionshlayout -> addWidget(svg_small_pictures, 5, 0);
|
||||||
|
|
||||||
vboxLayout -> addWidget(groupbox_options);
|
vboxLayout -> addWidget(groupbox_options);
|
||||||
|
|
||||||
setLayout(vboxLayout);
|
setLayout(vboxLayout);
|
||||||
@@ -252,6 +260,7 @@ void ExportPropertiesWidget::build()
|
|||||||
setTabOrder(draw_colored_conductors, draw_bg_transparent);
|
setTabOrder(draw_colored_conductors, draw_bg_transparent);
|
||||||
setTabOrder(draw_bg_transparent, dxf_blocks);
|
setTabOrder(draw_bg_transparent, dxf_blocks);
|
||||||
setTabOrder(dxf_blocks, dxf_attributes);
|
setTabOrder(dxf_blocks, dxf_attributes);
|
||||||
|
setTabOrder(draw_bg_transparent, svg_small_pictures);
|
||||||
|
|
||||||
// connexion du bouton permettant le choix du repertoire
|
// connexion du bouton permettant le choix du repertoire
|
||||||
connect(button_browse, &QPushButton::released, this, &ExportPropertiesWidget::slot_chooseADirectory);
|
connect(button_browse, &QPushButton::released, this, &ExportPropertiesWidget::slot_chooseADirectory);
|
||||||
@@ -269,6 +278,7 @@ void ExportPropertiesWidget::build()
|
|||||||
connect(draw_colored_conductors, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
|
connect(draw_colored_conductors, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
connect(dxf_blocks, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
|
connect(dxf_blocks, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
connect(dxf_attributes, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
|
connect(dxf_attributes, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
|
connect(svg_small_pictures, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
#else
|
#else
|
||||||
connect(draw_grid, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
connect(draw_grid, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
connect(draw_border, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
connect(draw_border, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
@@ -279,5 +289,6 @@ void ExportPropertiesWidget::build()
|
|||||||
connect(draw_colored_conductors, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
connect(draw_colored_conductors, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
connect(dxf_blocks, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
connect(dxf_blocks, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
connect(dxf_attributes, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
connect(dxf_attributes, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
|
connect(svg_small_pictures, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class ExportPropertiesWidget : public QWidget {
|
|||||||
QCheckBox *draw_colored_conductors;
|
QCheckBox *draw_colored_conductors;
|
||||||
QCheckBox *dxf_blocks;
|
QCheckBox *dxf_blocks;
|
||||||
QCheckBox *dxf_attributes;
|
QCheckBox *dxf_attributes;
|
||||||
|
QCheckBox *svg_small_pictures;
|
||||||
QRadioButton *export_border;
|
QRadioButton *export_border;
|
||||||
QRadioButton *export_elements;
|
QRadioButton *export_elements;
|
||||||
QButtonGroup *exported_content_choices;
|
QButtonGroup *exported_content_choices;
|
||||||
|
|||||||
@@ -232,6 +232,20 @@ void ProjectPrintWindow::requestPaint()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A PDF export turns each page to its folio's orientation (#1480): a
|
||||||
|
// portrait folio gets a portrait page instead of being shrunk onto a
|
||||||
|
// landscape one. Only the real export, with one page per folio: the
|
||||||
|
// preview draws every page at one paper size, and only Qt's PDF engine
|
||||||
|
// accepts a new page layout once printing has started.
|
||||||
|
const bool per_folio_orientation = m_printing
|
||||||
|
&& m_printer->outputFormat() == QPrinter::PdfFormat
|
||||||
|
&& ui->m_fit_in_page_cb->isChecked();
|
||||||
|
const auto chosen_orientation = m_printer->pageLayout().orientation();
|
||||||
|
const auto diagrams = selectedDiagram();
|
||||||
|
if (per_folio_orientation && !diagrams.isEmpty()) {
|
||||||
|
setOrientationForDiagram(diagrams.first());
|
||||||
|
}
|
||||||
|
|
||||||
bool first = true;
|
bool first = true;
|
||||||
QPainter painter(m_printer);
|
QPainter painter(m_printer);
|
||||||
int annotIndex = 0;
|
int annotIndex = 0;
|
||||||
@@ -245,12 +259,23 @@ void ProjectPrintWindow::requestPaint()
|
|||||||
// should be fixed by original author by evaluating function-result!
|
// should be fixed by original author by evaluating function-result!
|
||||||
(void)pdfExport;
|
(void)pdfExport;
|
||||||
|
|
||||||
for (auto diagram : selectedDiagram())
|
for (auto diagram : diagrams)
|
||||||
{
|
{
|
||||||
first ? first = false : m_printer->newPage();
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
if (per_folio_orientation) {
|
||||||
|
setOrientationForDiagram(diagram);
|
||||||
|
}
|
||||||
|
m_printer->newPage();
|
||||||
|
}
|
||||||
printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer, diagramPageMap, annotIndex);
|
printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer, diagramPageMap, annotIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (per_folio_orientation) {
|
||||||
|
m_printer->setPageOrientation(chosen_orientation);
|
||||||
|
}
|
||||||
|
|
||||||
// Note: do NOT call painter.end() or pdfConvertUriToGoTo() here.
|
// Note: do NOT call painter.end() or pdfConvertUriToGoTo() here.
|
||||||
// We are inside the paintRequested slot: the QPrintPreviewWidget still
|
// We are inside the paintRequested slot: the QPrintPreviewWidget still
|
||||||
// owns the paint cycle. On macOS arm64 (Metal/CALayer compositor),
|
// owns the paint cycle. On macOS arm64 (Metal/CALayer compositor),
|
||||||
@@ -259,6 +284,19 @@ void ProjectPrintWindow::requestPaint()
|
|||||||
// pdfConvertUriToGoTo() is deferred to print() via QTimer::singleShot(0).
|
// pdfConvertUriToGoTo() is deferred to print() via QTimer::singleShot(0).
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief ProjectPrintWindow::setOrientationForDiagram
|
||||||
|
* Turn the next page to portrait or landscape, as @diagram is drawn.
|
||||||
|
* @param diagram
|
||||||
|
*/
|
||||||
|
void ProjectPrintWindow::setOrientationForDiagram(Diagram *diagram)
|
||||||
|
{
|
||||||
|
const QRect rect = diagramRect(diagram, exportProperties());
|
||||||
|
m_printer->setPageOrientation(rect.height() > rect.width()
|
||||||
|
? QPageLayout::Portrait
|
||||||
|
: QPageLayout::Landscape);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief ProjectPrintWindow::printDiagram
|
* @brief ProjectPrintWindow::printDiagram
|
||||||
* Print @diagram on the @printer
|
* Print @diagram on the @printer
|
||||||
@@ -356,8 +394,12 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
|
|||||||
// est revenu a l'identite ici. On reconstruit DONC explicitement la
|
// est revenu a l'identite ici. On reconstruit DONC explicitement la
|
||||||
// transform appliquee par :
|
// transform appliquee par :
|
||||||
// diagram->render(painter, QRectF(), diagram_rect, KeepAspectRatio)
|
// diagram->render(painter, QRectF(), diagram_rect, KeepAspectRatio)
|
||||||
// cible vide => painter->viewport() ; source = diagram_rect ; centre.
|
// cible vide => taille du device ; source = diagram_rect ; centre.
|
||||||
const QRectF target = QRectF(painter->viewport());
|
// The target render() used for an empty one: the device's
|
||||||
|
// size. Not painter->viewport(), which is set once when the
|
||||||
|
// painter opens and misses a page turned since (#1480).
|
||||||
|
const QRectF target(0, 0, painter->device()->width(),
|
||||||
|
painter->device()->height());
|
||||||
const QRectF source = QRectF(diagram_rect); // meme source que render()
|
const QRectF source = QRectF(diagram_rect); // meme source que render()
|
||||||
|
|
||||||
// render() ANCRE en haut-gauche (pas de centrage) :
|
// render() ANCRE en haut-gauche (pas de centrage) :
|
||||||
@@ -852,8 +894,10 @@ void ProjectPrintWindow::print()
|
|||||||
const bool isPdf = (m_printer->outputFormat() == QPrinter::PdfFormat);
|
const bool isPdf = (m_printer->outputFormat() == QPrinter::PdfFormat);
|
||||||
const QString pdfFile = isPdf ? m_printer->outputFileName() : QString();
|
const QString pdfFile = isPdf ? m_printer->outputFileName() : QString();
|
||||||
|
|
||||||
|
m_printing = true;
|
||||||
m_preview->print(); // triggers requestPaint() synchronously; painter
|
m_preview->print(); // triggers requestPaint() synchronously; painter
|
||||||
// is created/destroyed inside that call
|
// is created/destroyed inside that call
|
||||||
|
m_printing = false;
|
||||||
|
|
||||||
savePageSetupForCurrentPrinter();
|
savePageSetupForCurrentPrinter();
|
||||||
savePrintProperties();
|
savePrintProperties();
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ class ProjectPrintWindow : public QMainWindow
|
|||||||
void saveReloadDiagramParameters(Diagram *diagram, const ExportProperties &options, bool save);
|
void saveReloadDiagramParameters(Diagram *diagram, const ExportProperties &options, bool save);
|
||||||
QList<Diagram *> selectedDiagram() const;
|
QList<Diagram *> selectedDiagram() const;
|
||||||
void exportToPDF();
|
void exportToPDF();
|
||||||
|
void setOrientationForDiagram(Diagram *diagram);
|
||||||
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -106,6 +107,7 @@ class ProjectPrintWindow : public QMainWindow
|
|||||||
QColor m_backup_diagram_background_color;
|
QColor m_backup_diagram_background_color;
|
||||||
QHash<Diagram *, QCheckBox *> m_diagram_list_hash;
|
QHash<Diagram *, QCheckBox *> m_diagram_list_hash;
|
||||||
QList<PdfLinks::ComponentInfo> m_componentInfoList;
|
QList<PdfLinks::ComponentInfo> m_componentInfoList;
|
||||||
|
bool m_printing = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // PROJECTPRINTWINDOW_H
|
#endif // PROJECTPRINTWINDOW_H
|
||||||
|
|||||||
+56
-3
@@ -341,13 +341,15 @@ QString ProjectView::askUserForFilePath(bool assign) {
|
|||||||
// ask the user for a filepath in order to save the project
|
// 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 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");
|
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(
|
QString filepath = QFileDialog::getSaveFileName(
|
||||||
this,
|
this,
|
||||||
tr("Save as", "dialog title"),
|
tr("Save as", "dialog title"),
|
||||||
m_project -> currentDir() + "/" + tr("untitled")
|
m_project -> currentDir() + "/" + tr("untitled")
|
||||||
+ (selected_filter == qetz_filter ? ".qetz" : ".qet"),
|
+ (selected_filter == qetz_filter ? ".qetz" : ".qet"),
|
||||||
qet_filter + ";;" + qetz_filter,
|
qetz_filter + ";;" + qet_filter,
|
||||||
&selected_filter
|
&selected_filter
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -725,6 +727,32 @@ QETResult ProjectView::saveAs()
|
|||||||
return(doSave());
|
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
|
Save project content, then write the project file. May
|
||||||
call saveAs if no filepath was provided before.
|
call saveAs if no filepath was provided before.
|
||||||
@@ -741,6 +769,31 @@ QETResult ProjectView::doSave()
|
|||||||
return(saveAs());
|
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
|
// write to file
|
||||||
QETResult result = m_project -> write();
|
QETResult result = m_project -> write();
|
||||||
updateWindowTitle();
|
updateWindowTitle();
|
||||||
@@ -1204,7 +1257,7 @@ void ProjectView::rebuildDiagramsMap()
|
|||||||
*/
|
*/
|
||||||
void ProjectView::tabChanged(int tab_id)
|
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
|
//its tab is first shown. Read the tab itself: the first tab is
|
||||||
//shown while loadDiagrams() is still filling m_diagram_ids
|
//shown while loadDiagrams() is still filling m_diagram_ids
|
||||||
if (auto dv = qobject_cast<DiagramView *>(m_tab->widget(tab_id)))
|
if (auto dv = qobject_cast<DiagramView *>(m_tab->widget(tab_id)))
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ class ProjectView : public QWidget
|
|||||||
QETResult save();
|
QETResult save();
|
||||||
QETResult saveAs();
|
QETResult saveAs();
|
||||||
QETResult doSave();
|
QETResult doSave();
|
||||||
|
QETResult saveCopyForOlderVersions();
|
||||||
int cleanProject();
|
int cleanProject();
|
||||||
void updateWindowTitle();
|
void updateWindowTitle();
|
||||||
void updateTabTitle(DiagramView *);
|
void updateTabTitle(DiagramView *);
|
||||||
|
|||||||
@@ -107,10 +107,11 @@ class TerminalData : public PropertiesInterface
|
|||||||
@brief m_potential
|
@brief m_potential
|
||||||
An optional, symbol-author-chosen grouping identifier shared by
|
An optional, symbol-author-chosen grouping identifier shared by
|
||||||
terminals that belong to the same physical terminal within
|
terminals that belong to the same physical terminal within
|
||||||
a multi-terminal block. When potential_isolating is enabled
|
a multi-terminal block. When the function separation checkbox
|
||||||
|
(element information potential_isolating) is enabled
|
||||||
on the parent element, terminals sharing a non-empty,
|
on the parent element, terminals sharing a non-empty,
|
||||||
matching m_potential stay linked to each other; terminals
|
matching m_potential stay linked to each other; terminals
|
||||||
with different (or empty) values become isolated from each
|
with different (or empty) values become separated from each
|
||||||
other. Empty by default.
|
other. Empty by default.
|
||||||
*/
|
*/
|
||||||
QString m_potential;
|
QString m_potential;
|
||||||
|
|||||||
@@ -978,6 +978,8 @@ void QETDiagramEditor::setUpActions()
|
|||||||
QAction *open_file = m_file_actions_group.addAction(QET::Icons::DocumentOpen, tr("&Open"));
|
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 = 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_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"));
|
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);
|
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_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 ->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_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_as, &QAction::triggered, this, &QETDiagramEditor::saveAs);
|
||||||
connect(m_save_file, &QAction::triggered, this, &QETDiagramEditor::save);
|
connect(m_save_file, &QAction::triggered, this, &QETDiagramEditor::save);
|
||||||
@@ -1969,6 +1984,16 @@ bool QETDiagramEditor::openAndAddProject(
|
|||||||
addProject(project);
|
addProject(project);
|
||||||
DialogWaiting::dropInstance();
|
DialogWaiting::dropInstance();
|
||||||
|
|
||||||
|
if (interactive && project->isFromLaterVersion()) {
|
||||||
|
QET::QetMessageBox::warning(
|
||||||
|
this,
|
||||||
|
tr("Opening the file read-only", "message box title"),
|
||||||
|
tr("This project was saved by a later version of QElectroTech. "
|
||||||
|
"It is opened read-only, so that saving it cannot lose what "
|
||||||
|
"this version does not know. Use Save As to keep a copy "
|
||||||
|
"in this version's format."));
|
||||||
|
}
|
||||||
|
|
||||||
//Report font descriptions which could not be read as-is (written by
|
//Report font descriptions which could not be read as-is (written by
|
||||||
//an incompatible Qt version or corrupted), so the user learns about
|
//an incompatible Qt version or corrupted), so the user learns about
|
||||||
//it from somewhere else than the console. See issue #553.
|
//it from somewhere else than the console. See issue #553.
|
||||||
@@ -2750,6 +2775,7 @@ void QETDiagramEditor::slot_updateActions()
|
|||||||
m_close_file-> setEnabled(opened_project);
|
m_close_file-> setEnabled(opened_project);
|
||||||
m_save_file-> setEnabled(opened_project);
|
m_save_file-> setEnabled(opened_project);
|
||||||
m_save_file_as-> 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_rotate_texts-> setEnabled(editable_project);
|
||||||
m_export_to_images-> setEnabled(opened_diagram);
|
m_export_to_images-> setEnabled(opened_diagram);
|
||||||
m_print-> setEnabled(opened_diagram);
|
m_print-> setEnabled(opened_diagram);
|
||||||
|
|||||||
@@ -301,6 +301,7 @@ class QETDiagramEditor : public QETMainWindow
|
|||||||
*m_close_file, ///< Close current project file
|
*m_close_file, ///< Close current project file
|
||||||
*m_save_file, ///< Save current project
|
*m_save_file, ///< Save current project
|
||||||
*m_save_file_as, ///< Save current project as a specific file
|
*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_find = nullptr,
|
||||||
*m_jump_to_element = nullptr; ///< Open the "jump to element" quick-open popup
|
*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
|
@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.
|
is built: read the database, as opening does once it has filled it.
|
||||||
Nothing for any other model.
|
Nothing for any other model.
|
||||||
*/
|
*/
|
||||||
@@ -391,7 +391,7 @@ void ProjectDBModel::fillValue()
|
|||||||
{
|
{
|
||||||
m_record.clear();
|
m_record.clear();
|
||||||
//Opening builds a table before the database is filled, and fills
|
//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
|
//the same, so that it is laid out as it would have been on
|
||||||
//opening -- empty until its folio is built (folioBuilt())
|
//opening -- empty until its folio is built (folioBuilt())
|
||||||
if (m_fill_when_built) {
|
if (m_fill_when_built) {
|
||||||
|
|||||||
@@ -566,7 +566,7 @@ void QetGraphicsTableItem::initLink()
|
|||||||
if (!m_pending_previous_table_uuid.isNull())
|
if (!m_pending_previous_table_uuid.isNull())
|
||||||
{
|
{
|
||||||
//The built folios only: a previous table on a folio not built
|
//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();
|
QETProject *project = this->diagram()->project();
|
||||||
ElementProvider provider_(project->builtFolios());
|
ElementProvider provider_(project->builtFolios());
|
||||||
auto previous_table = provider_.tableFromUuid(m_pending_previous_table_uuid);
|
auto previous_table = provider_.tableFromUuid(m_pending_previous_table_uuid);
|
||||||
@@ -583,7 +583,7 @@ void QetGraphicsTableItem::initLink()
|
|||||||
/**
|
/**
|
||||||
@brief QetGraphicsTableItem::previousTableUuid
|
@brief QetGraphicsTableItem::previousTableUuid
|
||||||
@return the uuid of the table before this one in its chain, linked or
|
@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
|
QUuid QetGraphicsTableItem::previousTableUuid() const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1154,6 +1154,11 @@ bool Conductor::fromXml(QDomElement &dom_element)
|
|||||||
|
|
||||||
m_freeze_label = dom_element.attribute("freezeLabel") == "true" ? true : false;
|
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);
|
setProperties(pr);
|
||||||
|
|
||||||
return retval;
|
return retval;
|
||||||
@@ -1857,7 +1862,7 @@ ConductorProperties Conductor::properties() const
|
|||||||
/**
|
/**
|
||||||
@brief Conductor::placedStore
|
@brief Conductor::placedStore
|
||||||
@return the project database holding this wire's properties
|
@return the project database holding this wire's properties
|
||||||
(DB-ACCESSORS-PLAN.md stage 4.5), or nullptr if it is not placed in a
|
or nullptr if it is not placed in a
|
||||||
project, or shares its uuid with another wire for the moment a copy
|
project, or shares its uuid with another wire for the moment a copy
|
||||||
takes to get its own
|
takes to get its own
|
||||||
*/
|
*/
|
||||||
@@ -2005,9 +2010,16 @@ void Conductor::displayedTextChanged()
|
|||||||
@param all_diagram : if true search in all diagram of the project,
|
@param all_diagram : if true search in all diagram of the project,
|
||||||
false search only in the parent diagram of this conductor
|
false search only in the parent diagram of this conductor
|
||||||
@param t_list : a list of terminal already found for this potential.
|
@param t_list : a list of terminal already found for this potential.
|
||||||
|
@param ignore_function_separation : if true, a terminal element whose
|
||||||
|
function separation is enabled does not stop the search, and the
|
||||||
|
conductors of both its sides are returned as one potential. Only
|
||||||
|
functionSeparatedConductors() passes true.
|
||||||
|
@sa functionSeparatedConductors
|
||||||
@return a QSet of conductor at the same potential.
|
@return a QSet of conductor at the same potential.
|
||||||
*/
|
*/
|
||||||
QSet<Conductor *> Conductor::relatedPotentialConductors(const bool all_diagram, QList <Terminal *> *t_list)
|
QSet<Conductor *> Conductor::relatedPotentialConductors(const bool all_diagram,
|
||||||
|
QList <Terminal *> *t_list,
|
||||||
|
const bool ignore_function_separation)
|
||||||
{
|
{
|
||||||
bool declar_t_list = false;
|
bool declar_t_list = false;
|
||||||
if (t_list == nullptr)
|
if (t_list == nullptr)
|
||||||
@@ -2030,7 +2042,8 @@ QSet<Conductor *> Conductor::relatedPotentialConductors(const bool all_diagram,
|
|||||||
|
|
||||||
//Get the other terminals of the parent element of @terminal, who share the same potential
|
//Get the other terminals of the parent element of @terminal, who share the same potential
|
||||||
//This is use for element type "folio report" and "terminal element"
|
//This is use for element type "folio report" and "terminal element"
|
||||||
for (Terminal *t : relatedPotentialTerminal(terminal, all_diagram))
|
for (Terminal *t : relatedPotentialTerminal(terminal, all_diagram,
|
||||||
|
ignore_function_separation))
|
||||||
{
|
{
|
||||||
if (!t_list->contains(t))
|
if (!t_list->contains(t))
|
||||||
{
|
{
|
||||||
@@ -2042,7 +2055,8 @@ QSet<Conductor *> Conductor::relatedPotentialConductors(const bool all_diagram,
|
|||||||
other_conductors_list_t.removeAll(this);
|
other_conductors_list_t.removeAll(this);
|
||||||
//Get the conductors at the same potential for each conductors of other_conductors_list_t
|
//Get the conductors at the same potential for each conductors of other_conductors_list_t
|
||||||
for (Conductor *c : other_conductors_list_t) {
|
for (Conductor *c : other_conductors_list_t) {
|
||||||
other_conductors += c->relatedPotentialConductors(all_diagram, t_list);
|
other_conductors += c->relatedPotentialConductors(all_diagram, t_list,
|
||||||
|
ignore_function_separation);
|
||||||
}
|
}
|
||||||
|
|
||||||
other_conductors += QSet<Conductor*>(other_conductors_list_t.begin(),other_conductors_list_t.end());
|
other_conductors += QSet<Conductor*>(other_conductors_list_t.begin(),other_conductors_list_t.end());
|
||||||
@@ -2055,6 +2069,36 @@ QSet<Conductor *> Conductor::relatedPotentialConductors(const bool all_diagram,
|
|||||||
return(other_conductors);
|
return(other_conductors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Conductor::functionSeparatedConductors
|
||||||
|
Return the conductors that belong to this conductor's potential only
|
||||||
|
by crossing a function separated terminal element -- a terminal
|
||||||
|
element whose function separation checkbox (element information
|
||||||
|
potential_isolating) is enabled, the checkbox the user ticks to stop
|
||||||
|
sharing a terminal's properties with the other side of the terminal.
|
||||||
|
|
||||||
|
Those conductors are deliberately not part of
|
||||||
|
relatedPotentialConductors(): no property may be copied over them
|
||||||
|
by default. One does cross that separation, though: the voltage /
|
||||||
|
protocol, which describes the electrical node itself. Callers that
|
||||||
|
push it over the whole potential use this method to reach the other
|
||||||
|
side (@see ConductorProperties::applyTensionProtocolChange).
|
||||||
|
|
||||||
|
It is simply the potential computed with the separations ignored,
|
||||||
|
minus the potential that respects them.
|
||||||
|
@param all_diagram : if true search in all diagram of the project,
|
||||||
|
false search only in the parent diagram of this conductor
|
||||||
|
@return a QSet of conductor, this conductor excluded
|
||||||
|
*/
|
||||||
|
QSet<Conductor *> Conductor::functionSeparatedConductors(const bool all_diagram)
|
||||||
|
{
|
||||||
|
QSet<Conductor *> separated = relatedPotentialConductors(all_diagram, nullptr, true);
|
||||||
|
separated.subtract(relatedPotentialConductors(all_diagram));
|
||||||
|
separated.remove(this);
|
||||||
|
|
||||||
|
return separated;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief Conductor::diagramEditor
|
@brief Conductor::diagramEditor
|
||||||
@return The parent diagram editor or nullptr;
|
@return The parent diagram editor or nullptr;
|
||||||
|
|||||||
@@ -151,7 +151,10 @@ class Conductor : public QGraphicsObject
|
|||||||
virtual void setHighlighted(Highlight);
|
virtual void setHighlighted(Highlight);
|
||||||
QSet<Conductor *> relatedPotentialConductors(
|
QSet<Conductor *> relatedPotentialConductors(
|
||||||
const bool all_diagram = true,
|
const bool all_diagram = true,
|
||||||
QList <Terminal *> *t_list=nullptr);
|
QList <Terminal *> *t_list=nullptr,
|
||||||
|
const bool ignore_function_separation = false);
|
||||||
|
QSet<Conductor *> functionSeparatedConductors(
|
||||||
|
const bool all_diagram = true);
|
||||||
QETDiagramEditor* diagramEditor() const;
|
QETDiagramEditor* diagramEditor() const;
|
||||||
void editProperty ();
|
void editProperty ();
|
||||||
|
|
||||||
|
|||||||
@@ -167,7 +167,28 @@ void DiagramImageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DarkImageRendering::paintPixmap(painter, pixmap_, m_adapt_to_dark_theme);
|
// SVG export with smaller pictures: a copy four times the size the
|
||||||
|
// picture is drawn at, when that is smaller than the picture. An
|
||||||
|
// SVG stores what it is given, so a photo shown at a tenth of its
|
||||||
|
// size would otherwise be written whole.
|
||||||
|
const QTransform world = painter -> worldTransform();
|
||||||
|
const qreal drawn_scale = std::sqrt(std::abs(world.determinant()));
|
||||||
|
const QSize small_size = (QSizeF(pixmap_.size()) * drawn_scale * 4).toSize();
|
||||||
|
if (diagram() && diagram() -> smallPictures() && !pixmap_.isNull()
|
||||||
|
&& small_size.width() < pixmap_.width()
|
||||||
|
&& small_size.height() < pixmap_.height()
|
||||||
|
&& !small_size.isEmpty())
|
||||||
|
{
|
||||||
|
const QPixmap small = pixmap_.scaled(small_size, Qt::IgnoreAspectRatio,
|
||||||
|
Qt::SmoothTransformation);
|
||||||
|
painter -> save();
|
||||||
|
painter -> scale(qreal(pixmap_.width()) / small.width(),
|
||||||
|
qreal(pixmap_.height()) / small.height());
|
||||||
|
DarkImageRendering::paintPixmap(painter, small, m_adapt_to_dark_theme);
|
||||||
|
painter -> restore();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
DarkImageRendering::paintPixmap(painter, pixmap_, m_adapt_to_dark_theme);
|
||||||
|
|
||||||
Q_UNUSED(option); Q_UNUSED(widget);
|
Q_UNUSED(option); Q_UNUSED(widget);
|
||||||
|
|
||||||
@@ -1787,6 +1808,71 @@ const QByteArray &DiagramImageItem::encodedPng(const QPixmap &pixmap, QByteArray
|
|||||||
@return true if successfully loaded.
|
@return true if successfully loaded.
|
||||||
*/
|
*/
|
||||||
bool DiagramImageItem::fromXml(const QDomElement &e)
|
bool DiagramImageItem::fromXml(const QDomElement &e)
|
||||||
|
{
|
||||||
|
return fromXml(e, QByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief DiagramImageItem::fromXml
|
||||||
|
Load this image from xml element e, its picture being @p picture, not
|
||||||
|
the base64 text of @p e, unless @p picture is empty. A .qetz keeps its
|
||||||
|
pictures as files (QetContainer::join()).
|
||||||
|
*/
|
||||||
|
bool DiagramImageItem::fromXml(const QDomElement &e, const QByteArray &picture)
|
||||||
|
{
|
||||||
|
Picture p = pictureOf(e, picture);
|
||||||
|
decodePicture(p);
|
||||||
|
return fromXml(e, std::move(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief DiagramImageItem::pictureOf
|
||||||
|
@return the picture of @p e, @p picture unless it is empty, else the
|
||||||
|
base64 text of @p e, still encoded : decodePicture() does the slow part.
|
||||||
|
Reads the DOM, so GUI thread only.
|
||||||
|
*/
|
||||||
|
DiagramImageItem::Picture DiagramImageItem::pictureOf(const QDomElement &e,
|
||||||
|
const QByteArray &picture)
|
||||||
|
{
|
||||||
|
Picture p;
|
||||||
|
p.bytes = picture;
|
||||||
|
if (p.bytes.isEmpty())
|
||||||
|
{
|
||||||
|
// From the first text node only, not e.text(): that also collects
|
||||||
|
// the text of every child element, so on a cropped or colour-keyed
|
||||||
|
// picture it carries <image_base>'s bytes too.
|
||||||
|
const QDomNode image_node = e.firstChild();
|
||||||
|
if (image_node.isText())
|
||||||
|
p.base64 = image_node.toText().data();
|
||||||
|
else
|
||||||
|
p.found = false;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief DiagramImageItem::decodePicture
|
||||||
|
Decodes the base64 text and the picture file of @p picture. Touches
|
||||||
|
nothing else, so it can run in any thread : a large photo takes about
|
||||||
|
a quarter of a second, and Diagram::fromXml() decodes a folio's
|
||||||
|
pictures in parallel.
|
||||||
|
*/
|
||||||
|
void DiagramImageItem::decodePicture(Picture &picture)
|
||||||
|
{
|
||||||
|
if (picture.bytes.isEmpty() && !picture.base64.isEmpty())
|
||||||
|
{
|
||||||
|
picture.bytes = QByteArray::fromBase64(picture.base64.toLatin1());
|
||||||
|
picture.base64.clear();
|
||||||
|
}
|
||||||
|
picture.image = QImage::fromData(picture.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief DiagramImageItem::fromXml
|
||||||
|
Load this image from xml element e, its picture already decoded by
|
||||||
|
decodePicture().
|
||||||
|
*/
|
||||||
|
bool DiagramImageItem::fromXml(const QDomElement &e, Picture picture)
|
||||||
{
|
{
|
||||||
if (e.tagName() != "image") {
|
if (e.tagName() != "image") {
|
||||||
return (false);
|
return (false);
|
||||||
@@ -1796,28 +1882,17 @@ bool DiagramImageItem::fromXml(const QDomElement &e)
|
|||||||
const QUuid uuid(e.attribute(QStringLiteral("uuid")));
|
const QUuid uuid(e.attribute(QStringLiteral("uuid")));
|
||||||
if (!uuid.isNull() && uuid != m_uuid) setUuid(uuid);
|
if (!uuid.isNull() && uuid != m_uuid) setUuid(uuid);
|
||||||
|
|
||||||
QDomNode image_node = e.firstChild();
|
if (!picture.found) {
|
||||||
if (!image_node.isText()) {
|
|
||||||
return (false);
|
return (false);
|
||||||
}
|
}
|
||||||
|
const QByteArray array = picture.bytes;
|
||||||
|
|
||||||
//load xml image to QByteArray
|
//Set QPixmap from the decoded picture
|
||||||
QByteArray array;
|
QPixmap pixmap = QPixmap::fromImage(std::move(picture.image));
|
||||||
array = QByteArray::fromBase64(e.text().toLatin1());
|
|
||||||
|
|
||||||
//Set QPixmap from the array
|
|
||||||
QPixmap pixmap;
|
|
||||||
pixmap.loadFromData(array);
|
|
||||||
setPixmap(pixmap);
|
setPixmap(pixmap);
|
||||||
// From the first text node only, not e.text(): that also collects the
|
if (isPng(array) && !pixmap_.isNull())
|
||||||
// text of every child element, so on a cropped or colour-keyed picture
|
|
||||||
// it carries <image_base>'s bytes too. loadFromData() stops at the end
|
|
||||||
// of the first PNG and never notices; a cache written back verbatim
|
|
||||||
// would.
|
|
||||||
const QByteArray ownArray = QByteArray::fromBase64(image_node.toText().data().toLatin1());
|
|
||||||
if (isPng(ownArray) && !pixmap_.isNull())
|
|
||||||
{
|
{
|
||||||
m_png_cache = ownArray;
|
m_png_cache = array;
|
||||||
m_png_cache_key = pixmap_.cacheKey();
|
m_png_cache_key = pixmap_.cacheKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
|
#include <QImage>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
#include <QStyleOptionGraphicsItem>
|
#include <QStyleOptionGraphicsItem>
|
||||||
@@ -119,7 +120,20 @@ class DiagramImageItem : public QetGraphicsItem {
|
|||||||
*/
|
*/
|
||||||
int type() const override { return Type; }
|
int type() const override { return Type; }
|
||||||
|
|
||||||
|
/// A picture as read from its <image> element, before and after
|
||||||
|
/// decodePicture(), which may run outside the GUI thread.
|
||||||
|
struct Picture {
|
||||||
|
QString base64; ///< text of <image>, decoded when bytes is empty
|
||||||
|
QByteArray bytes; ///< the picture file (PNG, JPEG...)
|
||||||
|
QImage image; ///< bytes, decoded
|
||||||
|
bool found = true; ///< false : neither bytes nor text
|
||||||
|
};
|
||||||
|
static Picture pictureOf(const QDomElement &, const QByteArray &picture);
|
||||||
|
static void decodePicture(Picture &picture);
|
||||||
|
|
||||||
virtual bool fromXml(const QDomElement &);
|
virtual bool fromXml(const QDomElement &);
|
||||||
|
bool fromXml(const QDomElement &, const QByteArray &picture);
|
||||||
|
bool fromXml(const QDomElement &, Picture picture);
|
||||||
virtual QDomElement toXml(QDomDocument &) const;
|
virtual QDomElement toXml(QDomDocument &) const;
|
||||||
QUuid uuid() const {return m_uuid;}
|
QUuid uuid() const {return m_uuid;}
|
||||||
void setUuid(const QUuid &uuid) {m_uuid = uuid; emit uuidChanged();}
|
void setUuid(const QUuid &uuid) {m_uuid = uuid; emit uuidChanged();}
|
||||||
|
|||||||
@@ -1115,6 +1115,11 @@ bool Element::fromXml(QDomElement &e,
|
|||||||
DiagramContext dc;
|
DiagramContext dc;
|
||||||
dc.fromXml(e.firstChildElement(QStringLiteral("elementInformations")),
|
dc.fromXml(e.firstChildElement(QStringLiteral("elementInformations")),
|
||||||
QStringLiteral("elementInformation"));
|
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)
|
//Load override properties (For now, only used when the element is a terminal)
|
||||||
if (m_data.m_type == ElementData::Terminal)
|
if (m_data.m_type == ElementData::Terminal)
|
||||||
@@ -1250,17 +1255,41 @@ QDomElement Element::toXml(
|
|||||||
|
|
||||||
//if this element is linked to other elements,
|
//if this element is linked to other elements,
|
||||||
//save the uuid of each 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"));
|
QDomElement links_uuids = document.createElement(QStringLiteral("links_uuids"));
|
||||||
foreach (Element *elmt, connected_elements) {
|
for (const auto &link : std::as_const(links)) {
|
||||||
QDomElement link_uuid =
|
QDomElement link_uuid =
|
||||||
document.createElement(QStringLiteral("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)
|
// Save group index if assigned (for slave->master links)
|
||||||
int gi = m_group_index_map.value(elmt, -1);
|
if (link.second >= 0) {
|
||||||
if (gi >= 0) {
|
link_uuid.setAttribute(QStringLiteral("group_index"), link.second);
|
||||||
link_uuid.setAttribute(QStringLiteral("group_index"), gi);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
links_uuids.appendChild(link_uuid);
|
links_uuids.appendChild(link_uuid);
|
||||||
@@ -1630,10 +1659,11 @@ void Element::initLink(QETProject *prj)
|
|||||||
// if nothing to link return now
|
// if nothing to link return now
|
||||||
if (tmp_uuids_link.isEmpty()) return;
|
if (tmp_uuids_link.isEmpty()) return;
|
||||||
|
|
||||||
//The built folios only: a partner on a folio not built yet
|
//The symbols built: on the folios built, and those built alone as
|
||||||
//(QET_LAZY_FOLIOS) is linked when its folio is, and the saved list
|
//partners on folios not built (Diagram::buildSymbols()). A partner
|
||||||
//is kept until then, for the order it gives
|
//not built yet (lazy folios) is linked when it is, and the
|
||||||
ElementProvider ep(prj->builtFolios());
|
//saved list is kept until then, for the order it gives
|
||||||
|
ElementProvider ep(prj->folios());
|
||||||
QList<QUuid> uuids;
|
QList<QUuid> uuids;
|
||||||
for (const auto &linkInfo : tmp_uuids_link) {
|
for (const auto &linkInfo : tmp_uuids_link) {
|
||||||
uuids.append(linkInfo.uuid);
|
uuids.append(linkInfo.uuid);
|
||||||
@@ -1659,7 +1689,7 @@ void Element::initLink(QETProject *prj)
|
|||||||
/**
|
/**
|
||||||
@brief Element::pendingLinks
|
@brief Element::pendingLinks
|
||||||
@return the uuids of the partners this symbol was saved linked to and
|
@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
|
until initLink() runs again once it is
|
||||||
*/
|
*/
|
||||||
QSet<QUuid> Element::pendingLinks() const
|
QSet<QUuid> Element::pendingLinks() const
|
||||||
@@ -1859,7 +1889,7 @@ void Element::setUuid(const QUuid &uuid)
|
|||||||
/**
|
/**
|
||||||
@brief Element::storeInformation
|
@brief Element::storeInformation
|
||||||
Write this symbol's information to its project's store, once it is
|
Write this symbol's information to its project's store, once it is
|
||||||
placed (DB-ACCESSORS-PLAN.md stage 4.1): every change of a placed
|
placed: every change of a placed
|
||||||
symbol's information comes through here.
|
symbol's information comes through here.
|
||||||
*/
|
*/
|
||||||
void Element::storeInformation()
|
void Element::storeInformation()
|
||||||
@@ -1925,7 +1955,7 @@ ElementData Element::elementData() const
|
|||||||
/**
|
/**
|
||||||
@brief Element::elementInformations
|
@brief Element::elementInformations
|
||||||
@return this symbol's information: for a placed symbol the project's
|
@return this symbol's information: for a placed symbol the project's
|
||||||
store holds it (DB-ACCESSORS-PLAN.md stage 4.2), for one not placed
|
store holds it, for one not placed
|
||||||
(a preview, the symbol editor) its own copy.
|
(a preview, the symbol editor) its own copy.
|
||||||
*/
|
*/
|
||||||
DiagramContext Element::elementInformations() const
|
DiagramContext Element::elementInformations() const
|
||||||
|
|||||||
@@ -1082,15 +1082,24 @@ void Terminal::setMasterLabelIndex(int index)
|
|||||||
@param terminal : to start search
|
@param terminal : to start search
|
||||||
@param all_diagram :if true return all related terminal,
|
@param all_diagram :if true return all related terminal,
|
||||||
false return only terminal in the same diagram of t
|
false return only terminal in the same diagram of t
|
||||||
|
@param ignore_function_separation : if true, a parent element whose
|
||||||
|
function separation (element information potential_isolating) is
|
||||||
|
enabled does not keep its terminals apart: they all count as one
|
||||||
|
potential, the way they do when the option is disabled. Only the
|
||||||
|
callers that need the conductors a separation hides -- so that the
|
||||||
|
one property still allowed to cross it, the voltage / protocol, can
|
||||||
|
be applied there (@see Conductor::functionSeparatedConductors) --
|
||||||
|
pass true; every other caller keeps the default.
|
||||||
@return the list of terminal at the same potential
|
@return the list of terminal at the same potential
|
||||||
*/
|
*/
|
||||||
QList<Terminal *> relatedPotentialTerminal (
|
QList<Terminal *> relatedPotentialTerminal (
|
||||||
const Terminal *terminal, const bool all_diagram)
|
const Terminal *terminal, const bool all_diagram,
|
||||||
|
const bool ignore_function_separation)
|
||||||
{
|
{
|
||||||
// If terminal parent element is a folio report.
|
// If terminal parent element is a folio report.
|
||||||
if (all_diagram && terminal -> parentElement() -> linkType() & Element::AllReport)
|
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
|
//is built, so that a potential is followed as far as when every
|
||||||
//folio is; not while folios are being read or built, which
|
//folio is; not while folios are being read or built, which
|
||||||
//would build whole report chains
|
//would build whole report chains
|
||||||
@@ -1102,6 +1111,14 @@ QList<Terminal *> relatedPotentialTerminal (
|
|||||||
project->buildFoliosHoldingSymbols(waiting);
|
project->buildFoliosHoldingSymbols(waiting);
|
||||||
}
|
}
|
||||||
QList <Element *> elmt_list = terminal -> parentElement() -> linkedElements();
|
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())
|
if (!elmt_list.isEmpty())
|
||||||
{
|
{
|
||||||
return (elmt_list.first()->terminals());
|
return (elmt_list.first()->terminals());
|
||||||
@@ -1110,14 +1127,18 @@ QList<Terminal *> relatedPotentialTerminal (
|
|||||||
// If terminal parent element is a Terminal element.
|
// If terminal parent element is a Terminal element.
|
||||||
else if (terminal -> parentElement() -> linkType() & Element::Terminale)
|
else if (terminal -> parentElement() -> linkType() & Element::Terminale)
|
||||||
{
|
{
|
||||||
// English: Check if the user activated the potential isolation checkbox for this terminal
|
// English: Check if the user activated the function separation
|
||||||
if (QET::infoFlagIsTrue(terminal->parentElement()->elementInformations().value(QStringLiteral("potential_isolating")).toString())) {
|
// checkbox for this terminal (the checkbox that used to be called
|
||||||
// English: Potential is isolated -- but terminals sharing this
|
// potential separation -- the stored flag is still
|
||||||
// terminal's own, non-empty potential-group identifier
|
// "potential_isolating" so existing elements keep their setting).
|
||||||
// (@see TerminalData::m_potential, set per-terminal in the
|
if (!ignore_function_separation
|
||||||
// element editor) still belong to the same physical terminal
|
&& QET::infoFlagIsTrue(terminal->parentElement()->elementInformations().value(QStringLiteral("potential_isolating")).toString())) {
|
||||||
|
// English: The terminals are function separated -- but terminals
|
||||||
|
// sharing this terminal's own, non-empty potential-group
|
||||||
|
// identifier (@see TerminalData::m_potential, set per-terminal in
|
||||||
|
// the element editor) still belong to the same physical terminal
|
||||||
// and stay linked to each other. An empty/unset potential on
|
// and stay linked to each other. An empty/unset potential on
|
||||||
// every terminal reproduces the previous, fully-isolated
|
// every terminal reproduces the previous, fully-separated
|
||||||
// behavior exactly, so existing elements are unaffected.
|
// behavior exactly, so existing elements are unaffected.
|
||||||
const QString this_potential = terminal->potential();
|
const QString this_potential = terminal->potential();
|
||||||
QList<Terminal *> same_potential;
|
QList<Terminal *> same_potential;
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ inline int Terminal::conductorsCount() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
QList<Terminal *> relatedPotentialTerminal (const Terminal *terminal,
|
QList<Terminal *> relatedPotentialTerminal (const Terminal *terminal,
|
||||||
const bool all_diagram = true);
|
const bool all_diagram = true,
|
||||||
|
const bool ignore_function_separation = false);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+437
-105
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
#include "qetproject.h"
|
#include "qetproject.h"
|
||||||
#include "container/qetcontainer.h"
|
#include "container/qetcontainer.h"
|
||||||
|
#include "container/qetcontainerdb.h"
|
||||||
|
|
||||||
#include "ElementsCollection/xmlelementcollection.h"
|
#include "ElementsCollection/xmlelementcollection.h"
|
||||||
#include "autoNum/assignvariables.h"
|
#include "autoNum/assignvariables.h"
|
||||||
@@ -30,6 +31,8 @@
|
|||||||
#include "autoNum/elementautonumschemecommand.h"
|
#include "autoNum/elementautonumschemecommand.h"
|
||||||
#include "diagram.h"
|
#include "diagram.h"
|
||||||
#include "qetgraphicsitem/element.h"
|
#include "qetgraphicsitem/element.h"
|
||||||
|
#include "qetgraphicsitem/conductor.h"
|
||||||
|
#include "qetgraphicsitem/dynamicelementtextitem.h"
|
||||||
#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
|
#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
|
||||||
#include "qetgraphicsitem/ViewItem/projectdbmodel.h"
|
#include "qetgraphicsitem/ViewItem/projectdbmodel.h"
|
||||||
#include "qetapp.h"
|
#include "qetapp.h"
|
||||||
@@ -46,6 +49,7 @@
|
|||||||
#include "qetversion.h"
|
#include "qetversion.h"
|
||||||
#include "undocommand/adddiagramcommand.h"
|
#include "undocommand/adddiagramcommand.h"
|
||||||
|
|
||||||
|
#include <QCryptographicHash>
|
||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
@@ -192,6 +196,8 @@ QETProject::QETProject(KAutoSaveFile *backup, QObject *parent) :
|
|||||||
}
|
}
|
||||||
//Set the real path, instead of the path of the backup.
|
//Set the real path, instead of the path of the backup.
|
||||||
setFilePath(backup->managedFile().path());
|
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;
|
delete backup;
|
||||||
|
|
||||||
//Set the project to read only mode if the file it.
|
//Set the project to read only mode if the file it.
|
||||||
@@ -278,6 +284,20 @@ QUuid QETProject::uuid() const
|
|||||||
return m_uuid;
|
return m_uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QETProject::containerPicture
|
||||||
|
The folio pictures of a .qetz are kept as bytes beside the document,
|
||||||
|
not as base64 text in it (QetContainer::join()), for as long as the
|
||||||
|
project is open: a folio built after opening still finds its pictures.
|
||||||
|
They cost almost no memory, sharing their bytes with the pictures
|
||||||
|
built from them.
|
||||||
|
@return the picture in @p file, or an empty array when there is none
|
||||||
|
*/
|
||||||
|
QByteArray QETProject::containerPicture(const QString &file) const
|
||||||
|
{
|
||||||
|
return m_container_pictures.value(file);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief QETProject::derivedItemUuid
|
@brief QETProject::derivedItemUuid
|
||||||
A uuid for an item of this project that was saved without one, the same
|
A uuid for an item of this project that was saved without one, the same
|
||||||
@@ -322,7 +342,7 @@ void QETProject::init()
|
|||||||
connect(&m_titleblocks_collection, &TitleBlockTemplatesCollection::changed, this, &QETProject::updateDiagramsTitleBlockTemplate);
|
connect(&m_titleblocks_collection, &TitleBlockTemplatesCollection::changed, this, &QETProject::updateDiagramsTitleBlockTemplate);
|
||||||
connect(&m_titleblocks_collection, &TitleBlockTemplatesCollection::aboutToRemove, this, &QETProject::removeDiagramsTitleBlockTemplate);
|
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,
|
//a folio's position, number or the folio total in their texts,
|
||||||
//the report and cross-reference settings, a symbol's definition.
|
//the report and cross-reference settings, a symbol's definition.
|
||||||
//A folio not built is then built to be saved (writesKeptFolios())
|
//A folio not built is then built to be saved (writesKeptFolios())
|
||||||
@@ -398,11 +418,25 @@ QUuid QETProject::derivedUuid(const QByteArray &content)
|
|||||||
static const QUuid project_namespace(
|
static const QUuid project_namespace(
|
||||||
QStringLiteral("{c8c75719-0fea-4b1c-9f4b-2dd179fb2f0c}"));
|
QStringLiteral("{c8c75719-0fea-4b1c-9f4b-2dd179fb2f0c}"));
|
||||||
|
|
||||||
QByteArray normalized(content);
|
//Same result as QUuid::createUuidV5(project_namespace,
|
||||||
normalized.replace('\r', QByteArray());
|
//"qet-project-legacy\n" + content without '\r'), hashed in place :
|
||||||
return QUuid::createUuidV5(project_namespace,
|
//copying the content twice took 2 s of opening a 263 MB project.
|
||||||
QByteArrayLiteral("qet-project-legacy\n")
|
QCryptographicHash hash(QCryptographicHash::Sha1);
|
||||||
+ normalized);
|
hash.addData(project_namespace.toRfc4122());
|
||||||
|
hash.addData(QByteArrayView("qet-project-legacy\n"));
|
||||||
|
const QByteArrayView view(content);
|
||||||
|
qsizetype start = 0;
|
||||||
|
while (start < view.size()) {
|
||||||
|
qsizetype cr = view.indexOf('\r', start);
|
||||||
|
if (cr < 0)
|
||||||
|
cr = view.size();
|
||||||
|
hash.addData(view.sliced(start, cr - start));
|
||||||
|
start = cr + 1;
|
||||||
|
}
|
||||||
|
QByteArray bytes = hash.result().left(16);
|
||||||
|
bytes[6] = char((bytes[6] & 0x0F) | 0x50); // version 5
|
||||||
|
bytes[8] = char((bytes[8] & 0x3F) | 0x80); // RFC 4122 variant
|
||||||
|
return QUuid::fromRfc4122(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -437,8 +471,12 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
|
|||||||
//Extract the content of the xml. The raw bytes are kept : a project
|
//Extract the content of the xml. The raw bytes are kept : a project
|
||||||
//file without a persisted uuid derives its uuid from them.
|
//file without a persisted uuid derives its uuid from them.
|
||||||
const QByteArray content = file->readAll();
|
const QByteArray content = file->readAll();
|
||||||
//Each folio's text is kept while it is not built (readDiagramsXml())
|
//Each folio's text is kept while it is not built (readDiagramsXml()):
|
||||||
m_opened_content = content;
|
//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;
|
QDomDocument xml_project;
|
||||||
// PreserveSpacingOnlyNodes: without it, a text node that is entirely
|
// PreserveSpacingOnlyNodes: without it, a text node that is entirely
|
||||||
// whitespace -- e.g. a title-block custom variable deliberately set to
|
// whitespace -- e.g. a title-block custom variable deliberately set to
|
||||||
@@ -456,9 +494,28 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
|
|||||||
// QetContainer::parse() does exactly that, for a .qet and for each part
|
// QetContainer::parse() does exactly that, for a .qet and for each part
|
||||||
// of a .qetz alike.
|
// of a .qetz alike.
|
||||||
QString read_error;
|
QString read_error;
|
||||||
if (!QetContainer::readDocument(content, &xml_project, &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::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;
|
qWarning().noquote() << "Unable to read" << fi.fileName() << ":" << read_error;
|
||||||
|
m_container_pictures.clear();
|
||||||
|
m_folio_parts.clear();
|
||||||
if(opened_here) {
|
if(opened_here) {
|
||||||
file->close();
|
file->close();
|
||||||
}
|
}
|
||||||
@@ -489,9 +546,16 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
|
|||||||
<< xml_parse_ms / 1000.0 << ", content "
|
<< xml_parse_ms / 1000.0 << ", content "
|
||||||
<< (total_ms - xml_parse_ms) / 1000.0 << ")";
|
<< (total_ms - xml_parse_ms) / 1000.0 << ")";
|
||||||
|
|
||||||
if (!fi.isWritable()) {
|
//Written by a later QElectroTech that lets this one read it: saving
|
||||||
|
//here would drop what this version does not know
|
||||||
|
m_from_later_version = container_format > QetContainer::Format;
|
||||||
|
if (!fi.isWritable() || m_from_later_version) {
|
||||||
setReadOnly(true);
|
setReadOnly(true);
|
||||||
}
|
}
|
||||||
|
if (m_from_later_version) {
|
||||||
|
qInfo().noquote() << QStringLiteral("Project %1 is of .qetz format %2 (this version writes %3): opened read-only")
|
||||||
|
.arg(fi.fileName()).arg(container_format).arg(QetContainer::Format);
|
||||||
|
}
|
||||||
if(opened_here) {
|
if(opened_here) {
|
||||||
file->close();
|
file->close();
|
||||||
}
|
}
|
||||||
@@ -544,7 +608,7 @@ QETProject::ProjectState QETProject::state() const
|
|||||||
QList<Diagram *> QETProject::diagrams() const
|
QList<Diagram *> QETProject::diagrams() const
|
||||||
{
|
{
|
||||||
//Every caller of diagrams() expects every folio built: those that
|
//Every caller of diagrams() expects every folio built: those that
|
||||||
//do not are moved to folios() one by one (LAZY-FOLIO-PLAN.md)
|
//do not are moved to folios() one by one
|
||||||
loadFolios();
|
loadFolios();
|
||||||
return(m_diagrams_list);
|
return(m_diagrams_list);
|
||||||
}
|
}
|
||||||
@@ -552,7 +616,7 @@ QList<Diagram *> QETProject::diagrams() const
|
|||||||
/**
|
/**
|
||||||
@brief QETProject::folios
|
@brief QETProject::folios
|
||||||
@return the folios of this project, whether their items are built yet
|
@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
|
QList<Diagram *> QETProject::folios() const
|
||||||
{
|
{
|
||||||
@@ -564,9 +628,9 @@ QList<Diagram *> QETProject::folios() const
|
|||||||
@return the folios CableManager::refreshLabels() works on, built: every
|
@return the folios CableManager::refreshLabels() works on, built: every
|
||||||
folio when the project has cables (a core is followed through folio
|
folio when the project has cables (a core is followed through folio
|
||||||
reports to any folio); without cables, the folios built and those not
|
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
|
built yet holding a wire that still refers to a cable -- the only
|
||||||
cable -- the only wires the pass changes then, taking the reference
|
wires the pass changes then, taking the reference away, read from the
|
||||||
away -- found in the XML kept for them
|
folios' XML.
|
||||||
*/
|
*/
|
||||||
QList<Diagram *> QETProject::cableLabelFolios()
|
QList<Diagram *> QETProject::cableLabelFolios()
|
||||||
{
|
{
|
||||||
@@ -590,7 +654,7 @@ QList<Diagram *> QETProject::cableLabelFolios()
|
|||||||
/**
|
/**
|
||||||
@brief QETProject::builtFolios
|
@brief QETProject::builtFolios
|
||||||
@return the folios of this project whose items are built: all of them,
|
@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
|
QList<Diagram *> QETProject::builtFolios() const
|
||||||
{
|
{
|
||||||
@@ -683,6 +747,21 @@ void QETProject::buildFolios(const QList<Diagram *> &folios) const
|
|||||||
for (Element *elmt : d->elements())
|
for (Element *elmt : d->elements())
|
||||||
if (elmt->keepsSavedLinks())
|
if (elmt->keepsSavedLinks())
|
||||||
elmt->initLink(self);
|
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
|
//Likewise a table whose previous table was on a folio not built
|
||||||
for (Diagram *d : builtFolios())
|
for (Diagram *d : builtFolios())
|
||||||
for (QGraphicsItem *item : d->items())
|
for (QGraphicsItem *item : d->items())
|
||||||
@@ -707,7 +786,7 @@ void QETProject::buildFolios(const QList<Diagram *> &folios) const
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
@brief QETProject::buildFoliosHolding
|
@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
|
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
|
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
|
in the project. What a search for such symbols must build to find
|
||||||
@@ -734,29 +813,22 @@ void QETProject::buildFoliosHolding(ElementData::Types kinds, bool free_only)
|
|||||||
for (auto it = types.cbegin() ; it != types.cend() ; ++it)
|
for (auto it = types.cbegin() ; it != types.cend() ; ++it)
|
||||||
placed.insert(it.key());
|
placed.insert(it.key());
|
||||||
}
|
}
|
||||||
|
//The symbols and their links as the database has them
|
||||||
QList<Diagram *> to_build;
|
QList<Diagram *> to_build;
|
||||||
for (Diagram *d : std::as_const(m_diagrams_list))
|
for (Diagram *d : std::as_const(m_diagrams_list))
|
||||||
{
|
{
|
||||||
if (d->isLoaded()) {
|
if (d->isLoaded()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const QDomElement &element_xml :
|
for (const projectDataBase::FolioSymbol &symbol : m_data_base.symbolsOnFolios({d->uuid()}))
|
||||||
QET::findInDomElement(d->deferredXml(),
|
|
||||||
QStringLiteral("elements"),
|
|
||||||
QStringLiteral("element")))
|
|
||||||
{
|
{
|
||||||
const auto type = types.constFind(QUuid(element_xml.attribute(QStringLiteral("uuid"))));
|
if (!(kinds & ElementData::typeFromString(symbol.kind))) {
|
||||||
if (type == types.constEnd()
|
|
||||||
|| !(kinds & ElementData::typeFromString(*type))) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
bool linked = false;
|
bool linked = false;
|
||||||
if (free_only) {
|
if (free_only) {
|
||||||
for (const QDomElement &link :
|
for (const QUuid &link : m_data_base.linkedSymbols(symbol.uuid)) {
|
||||||
QET::findInDomElement(element_xml,
|
if (placed.contains(link)) {
|
||||||
QStringLiteral("links_uuids"),
|
|
||||||
QStringLiteral("link_uuid"))) {
|
|
||||||
if (placed.contains(QUuid(link.attribute(QStringLiteral("uuid"))))) {
|
|
||||||
linked = true;
|
linked = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -781,8 +853,9 @@ void QETProject::buildFoliosHolding(ElementData::Types kinds, bool free_only)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
@brief QETProject::buildFoliosHoldingSymbols
|
@brief QETProject::buildFoliosHoldingSymbols
|
||||||
Build the folios not built yet (QET_LAZY_FOLIOS) holding one of the
|
Build the folios not built yet (lazy folios) holding one of the
|
||||||
symbols @p symbols, found in the XML kept for them: what needs those
|
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
|
symbols while the project is being read, before the database knows
|
||||||
where they are (a terminal strip's terminals)
|
where they are (a terminal strip's terminals)
|
||||||
*/
|
*/
|
||||||
@@ -792,6 +865,15 @@ void QETProject::buildFoliosHoldingSymbols(const QSet<QUuid> &symbols)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
QList<Diagram *> to_build;
|
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))
|
for (Diagram *d : std::as_const(m_diagrams_list))
|
||||||
{
|
{
|
||||||
if (d->isLoaded()) {
|
if (d->isLoaded()) {
|
||||||
@@ -820,7 +902,7 @@ void QETProject::buildFoliosHoldingSymbols(const QSet<QUuid> &symbols)
|
|||||||
/**
|
/**
|
||||||
@brief QETProject::symbolsOnUnbuiltFolios
|
@brief QETProject::symbolsOnUnbuiltFolios
|
||||||
@return the uuids of the symbols on the folios not built yet
|
@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
|
QSet<QUuid> QETProject::symbolsOnUnbuiltFolios() const
|
||||||
{
|
{
|
||||||
@@ -928,11 +1010,43 @@ void QETProject::buildFolioToShow(Diagram *folio)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS_TRACE") == 1) {
|
if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS_TRACE") == 1) {
|
||||||
qWarning().noquote() << "lazy folios:" << to_build.size()
|
qWarning().noquote() << "lazy folios: the partners of folio" << folio->folioIndex() + 1
|
||||||
<< "folios built for the partners of folio"
|
<< "built alone, on" << to_build.size() << "folios";
|
||||||
<< folio->folioIndex() + 1;
|
|
||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1007,6 +1121,8 @@ void QETProject::setFilePath(const QString &filepath)
|
|||||||
}
|
}
|
||||||
m_next_backup_slot = 0;
|
m_next_backup_slot = 0;
|
||||||
m_file_path = filepath;
|
m_file_path = filepath;
|
||||||
|
//A path chosen is saved to as it is
|
||||||
|
m_opened_from_qet = false;
|
||||||
|
|
||||||
QFileInfo fi(m_file_path);
|
QFileInfo fi(m_file_path);
|
||||||
if (fi.isWritable()) {
|
if (fi.isWritable()) {
|
||||||
@@ -1774,9 +1890,9 @@ std::optional<QUuid> QETProject::elementAutoNumIdToLink(
|
|||||||
@brief QETProject::symbolNumbering
|
@brief QETProject::symbolNumbering
|
||||||
@return the numbering of every symbol placed on the folios, read from
|
@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
|
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,
|
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
|
QList<QETProject::SymbolNumbering> QETProject::symbolNumbering() const
|
||||||
{
|
{
|
||||||
@@ -1807,38 +1923,27 @@ QList<QETProject::SymbolNumbering> QETProject::symbolNumbering() const
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
//A symbol the database has no row for is one building leaves out
|
//The symbols of the folios not built, from the database. A symbol
|
||||||
//(its definition is missing)
|
//it has no row for is one building leaves out (its definition is
|
||||||
const QHash<QUuid, QString> types = m_data_base.elementTypesOnFolios(unbuilt);
|
//missing)
|
||||||
const QHash<QString, QStringList> titles_by_formula = elementAutoNumTitlesByFormula();
|
QList<QUuid> unbuilt_in_order;
|
||||||
for (Diagram *d : m_diagrams_list)
|
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()) {
|
const DiagramContext info = m_data_base.elementInformation(row.uuid);
|
||||||
continue;
|
SymbolNumbering symbol;
|
||||||
}
|
symbol.label = info.value(QETInformation::ELMT_LABEL).toString();
|
||||||
for (const QDomElement &element_xml :
|
symbol.formula = info.value(QETInformation::ELMT_FORMULA).toString();
|
||||||
QET::findInDomElement(d->deferredXml(),
|
const auto linked = elementAutoNumIdToLink(info, titles_by_formula);
|
||||||
QStringLiteral("elements"),
|
symbol.scheme = linked ? *linked
|
||||||
QStringLiteral("element")))
|
: QUuid(info.value(QETInformation::ELMT_FORMULA_ID).toString());
|
||||||
{
|
symbol.sequence = row.sequence;
|
||||||
const QUuid uuid(element_xml.attribute(QStringLiteral("uuid")));
|
const ElementData::Type kind = ElementData::typeFromString(row.kind);
|
||||||
const auto type = types.constFind(uuid);
|
symbol.numbered = kind != ElementData::Slave
|
||||||
if (type == types.constEnd()) {
|
&& !(kind & ElementData::AllReport);
|
||||||
continue;
|
list << symbol;
|
||||||
}
|
|
||||||
const DiagramContext info = m_data_base.elementInformation(uuid);
|
|
||||||
SymbolNumbering symbol;
|
|
||||||
symbol.label = info.value(QETInformation::ELMT_LABEL).toString();
|
|
||||||
symbol.formula = info.value(QETInformation::ELMT_FORMULA).toString();
|
|
||||||
const auto linked = elementAutoNumIdToLink(info, titles_by_formula);
|
|
||||||
symbol.scheme = linked ? *linked
|
|
||||||
: QUuid(info.value(QETInformation::ELMT_FORMULA_ID).toString());
|
|
||||||
symbol.sequence.fromXml(element_xml.firstChildElement(QStringLiteral("sequentialNumbers")));
|
|
||||||
const ElementData::Type kind = ElementData::typeFromString(*type);
|
|
||||||
symbol.numbered = kind != ElementData::Slave
|
|
||||||
&& !(kind & ElementData::AllReport);
|
|
||||||
list << symbol;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
@@ -2237,9 +2342,13 @@ void QETProject::autoFolioNumberingSelectedFolios(int from,
|
|||||||
@brief QETProject::toXml
|
@brief QETProject::toXml
|
||||||
@return un document XML representant le projet
|
@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
|
//written as a placeholder <diagram qet_kept_folio="n"/> and its text
|
||||||
//as read from the file goes in kept_folios, for
|
//as read from the file goes in kept_folios, for
|
||||||
//spliceKeptFolios() -- when building it would write that same
|
//spliceKeptFolios() -- when building it would write that same
|
||||||
@@ -2252,31 +2361,58 @@ QDomDocument QETProject::toXml(QHash<int, QByteArray> *kept_folios)
|
|||||||
//is built with the folios its links reach, as when it is shown:
|
//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
|
//their saved texts can show its data (a report, its partner's
|
||||||
//folio label).
|
//folio label).
|
||||||
const bool keep = kept_folios && unloadedFolioCount()
|
const bool keep = unloadedFolioCount() && !m_kept_folios_stale
|
||||||
&& m_kept_folios_trusted && !m_kept_folios_stale;
|
&& ((kept_folios && m_kept_folios_trusted) || (kept_parts && m_kept_parts_trusted));
|
||||||
if (keep) {
|
if (keep) {
|
||||||
for (Diagram *diagram : std::as_const(m_diagrams_list))
|
//Partners built alone and linked or unlinked since: their folio
|
||||||
if (diagram->headerChanged())
|
//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);
|
buildFolioToShow(diagram);
|
||||||
|
}
|
||||||
//A table filled from the database saves the size its rows
|
//A table filled from the database saves the size its rows
|
||||||
//give it, which any edit can change
|
//give it, which any edit can change
|
||||||
if (m_edited_since_open)
|
if (m_edited_since_open)
|
||||||
for (Diagram *diagram : std::as_const(m_diagrams_list))
|
for (Diagram *diagram : std::as_const(m_diagrams_list))
|
||||||
if (!diagram->isLoaded() && !diagram->deferredXml().elementsByTagName(
|
if (!diagram->isLoaded() && unbuiltFolioHasTables(diagram))
|
||||||
QetGraphicsTableItem::xmlTagName()).isEmpty())
|
|
||||||
buildFolioToShow(diagram);
|
buildFolioToShow(diagram);
|
||||||
//A built folio is written from its items, which need their
|
//A built folio is written from its items, which need their
|
||||||
//partners: a symbol saves its links, a report the position of
|
//partners: a table saves the table before it, and in a .qet a
|
||||||
//its partner. Build the folios of every partner still waiting,
|
//report the position of its partner. Build the folios of every
|
||||||
//and of every table chain, until none is
|
//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() ; )
|
for (int built = -1 ; built != unloadedFolioCount() ; )
|
||||||
{
|
{
|
||||||
built = unloadedFolioCount();
|
built = unloadedFolioCount();
|
||||||
QSet<QUuid> waiting;
|
QSet<QUuid> waiting;
|
||||||
QSet<Diagram *> chained;
|
QSet<Diagram *> chained;
|
||||||
for (Diagram *diagram : builtFolios()) {
|
for (Diagram *diagram : builtFolios()) {
|
||||||
for (Element *element : diagram->elements())
|
if (kept_folios)
|
||||||
waiting.unite(element->pendingLinks());
|
for (Element *element : diagram->elements())
|
||||||
|
waiting.unite(element->pendingLinks());
|
||||||
for (Diagram *other : tableChainFolios(diagram))
|
for (Diagram *other : tableChainFolios(diagram))
|
||||||
if (!other->isLoaded()) chained.insert(other);
|
if (!other->isLoaded()) chained.insert(other);
|
||||||
}
|
}
|
||||||
@@ -2290,9 +2426,9 @@ QDomDocument QETProject::toXml(QHash<int, QByteArray> *kept_folios)
|
|||||||
} else {
|
} else {
|
||||||
loadFolios();
|
loadFolios();
|
||||||
}
|
}
|
||||||
|
m_waiting_link_targets = unloadedFolioCount() ? symbolsOnUnbuiltFolios() : QSet<QUuid>();
|
||||||
//QET_CHECK_ELEMENT_INFO_STORE=1: say whether the stores of symbol
|
//QET_CHECK_ELEMENT_INFO_STORE=1: say whether the stores of symbol
|
||||||
//information, folio title blocks and wire properties
|
//information, folio title blocks and wire properties agree with every placed symbol, folio and
|
||||||
//(DB-ACCESSORS-PLAN.md) agree with every placed symbol, folio and
|
|
||||||
//wire, at each save. A test and debugging aid; it changes nothing.
|
//wire, at each save. A test and debugging aid; it changes nothing.
|
||||||
if (qEnvironmentVariableIntValue("QET_CHECK_ELEMENT_INFO_STORE") == 1) {
|
if (qEnvironmentVariableIntValue("QET_CHECK_ELEMENT_INFO_STORE") == 1) {
|
||||||
const QStringList mismatches = m_data_base.elementInformationMismatches();
|
const QStringList mismatches = m_data_base.elementInformationMismatches();
|
||||||
@@ -2368,6 +2504,65 @@ QDomDocument QETProject::toXml(QHash<int, QByteArray> *kept_folios)
|
|||||||
<< "["
|
<< "["
|
||||||
<< diagram
|
<< 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()) {
|
if (keep && !diagram->isLoaded()) {
|
||||||
auto placeholder = xml_doc.createElement(QStringLiteral("diagram"));
|
auto placeholder = xml_doc.createElement(QStringLiteral("diagram"));
|
||||||
placeholder.setAttribute(QStringLiteral("qet_kept_folio"), order_num);
|
placeholder.setAttribute(QStringLiteral("qet_kept_folio"), order_num);
|
||||||
@@ -2409,6 +2604,7 @@ QDomDocument QETProject::toXml(QHash<int, QByteArray> *kept_folios)
|
|||||||
// Write the elements collection.
|
// Write the elements collection.
|
||||||
project_root.appendChild(m_elements_collection->root().cloneNode(true));
|
project_root.appendChild(m_elements_collection->root().cloneNode(true));
|
||||||
|
|
||||||
|
m_waiting_link_targets.clear();
|
||||||
return(xml_doc);
|
return(xml_doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2464,21 +2660,72 @@ bool QETProject::isContainerPath(const QString &path)
|
|||||||
document, in parts, zipped (#1440). The file is replaced only once
|
document, in parts, zipped (#1440). The file is replaced only once
|
||||||
the whole archive is written.
|
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,
|
project,
|
||||||
QStringLiteral("QElectroTech %1").arg(QetVersion::currentVersion().toString()),
|
QStringLiteral("QElectroTech %1").arg(QetVersion::currentVersion().toString()),
|
||||||
error);
|
error, kept_from);
|
||||||
return !entries.isEmpty() && QetZip::write(path, entries, error);
|
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()
|
QETResult QETProject::write()
|
||||||
{
|
{
|
||||||
// this operation requires a filepath
|
// this operation requires a filepath
|
||||||
if (m_file_path.isEmpty())
|
if (m_file_path.isEmpty())
|
||||||
return(QString("unable to save project to file: no filepath was specified"));
|
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
|
// 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
|
// 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
|
// new file (e.g. "Save As" to another location) whose directory is not
|
||||||
@@ -2496,13 +2743,15 @@ QETResult QETProject::write()
|
|||||||
QString error_message;
|
QString error_message;
|
||||||
if (isContainerPath(m_file_path))
|
if (isContainerPath(m_file_path))
|
||||||
{
|
{
|
||||||
// A container must contain complete folios, not lazy placeholders.
|
//A container is written from the whole document: every folio
|
||||||
QDomDocument xml_project(toXml());
|
//built (stage D4 writes it from the database)
|
||||||
if (!writeContainer(xml_project, m_file_path, &error_message))
|
if (!writeContainerFile(m_file_path, &error_message))
|
||||||
return(error_message);
|
return(error_message);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
//Folios not built (lazy folios) written as read when that
|
||||||
|
//is what building them would write (toXml())
|
||||||
QHash<int, QByteArray> kept_folios;
|
QHash<int, QByteArray> kept_folios;
|
||||||
QDomDocument xml_project(toXml(&kept_folios));
|
QDomDocument xml_project(toXml(&kept_folios));
|
||||||
if (!QET::writeXmlText(spliceKeptFolios(xml_project.toString(4), kept_folios),
|
if (!QET::writeXmlText(spliceKeptFolios(xml_project.toString(4), kept_folios),
|
||||||
@@ -2743,24 +2992,77 @@ bool QETProject::usesElement(const ElementsLocation &location) const
|
|||||||
return(unbuiltFolioElements().contains(location));
|
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
|
@brief QETProject::unbuiltFolioElements
|
||||||
@return the locations of the symbols placed on the folios not built yet
|
@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
|
(lazy folios), each once, read from the project database once it
|
||||||
building them reads it (Diagram::fromXml())
|
holds those folios, else from the XML kept for them as building them
|
||||||
|
reads it (Diagram::fromXml())
|
||||||
*/
|
*/
|
||||||
QList<ElementsLocation> QETProject::unbuiltFolioElements() const
|
QList<ElementsLocation> QETProject::unbuiltFolioElements() const
|
||||||
{
|
{
|
||||||
QSet<QString> types;
|
QSet<QString> types;
|
||||||
for (Diagram *diagram : m_diagrams_list) {
|
if (m_data_base.rowsFromDocument()) {
|
||||||
if (diagram->isLoaded()) {
|
QList<QUuid> unbuilt;
|
||||||
continue;
|
for (Diagram *diagram : m_diagrams_list)
|
||||||
}
|
if (!diagram->isLoaded()) unbuilt << diagram->uuid();
|
||||||
for (const QDomElement &element_xml :
|
for (const projectDataBase::FolioSymbol &symbol : m_data_base.symbolsOnFolios(unbuilt))
|
||||||
QET::findInDomElement(diagram->deferredXml(),
|
types.insert(symbol.location);
|
||||||
QStringLiteral("elements"),
|
} else {
|
||||||
QStringLiteral("element"))) {
|
for (Diagram *diagram : m_diagrams_list) {
|
||||||
types.insert(element_xml.attribute(QStringLiteral("type")));
|
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;
|
QList<ElementsLocation> locations;
|
||||||
@@ -3015,9 +3317,22 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
|
|||||||
|
|
||||||
//The stores of symbol information, folio title blocks and wire
|
//The stores of symbol information, folio title blocks and wire
|
||||||
//properties are filled from the document before the folios are
|
//properties are filled from the document before the folios are
|
||||||
//built (DB-ACCESSORS-PLAN.md stage 4.6)
|
//built
|
||||||
m_data_base.prefillFromDocument(xml_project);
|
m_data_base.prefillFromDocument(xml_project);
|
||||||
|
|
||||||
|
//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 = lazyFolios();
|
||||||
|
if (m_lazy_open) {
|
||||||
|
const QString why = projectDataBase::documentRefusal(xml_project);
|
||||||
|
if (!why.isEmpty()) {
|
||||||
|
qInfo().noquote() << "Project opened with every folio built:" << why;
|
||||||
|
m_lazy_open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Load the diagrams
|
//Load the diagrams
|
||||||
readDiagramsXml(xml_project);
|
readDiagramsXml(xml_project);
|
||||||
const qint64 diagrams_ms = phase_timer.restart();
|
const qint64 diagrams_ms = phase_timer.restart();
|
||||||
@@ -3057,7 +3372,6 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
|
|||||||
//number can hold what a save rewrites (tst_freetextnonfinite,
|
//number can hold what a save rewrites (tst_freetextnonfinite,
|
||||||
//tst_foliofreezeflags): only with QET_LAZY_SAVE_KEPT=1, until a
|
//tst_foliofreezeflags): only with QET_LAZY_SAVE_KEPT=1, until a
|
||||||
//.qetz, whose manifest names its writer, does it
|
//.qetz, whose manifest names its writer, does it
|
||||||
//(QETZ-DB-TRUTH-PLAN.md D4)
|
|
||||||
m_kept_folios_trusted = qEnvironmentVariableIntValue("QET_LAZY_SAVE_KEPT") == 1
|
m_kept_folios_trusted = qEnvironmentVariableIntValue("QET_LAZY_SAVE_KEPT") == 1
|
||||||
&& unloadedFolioCount() > 0
|
&& unloadedFolioCount() > 0
|
||||||
&& m_project_qet_version == QetVersion::currentVersion()
|
&& m_project_qet_version == QetVersion::currentVersion()
|
||||||
@@ -3065,9 +3379,16 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
|
|||||||
for (Diagram *diagram : std::as_const(m_diagrams_list))
|
for (Diagram *diagram : std::as_const(m_diagrams_list))
|
||||||
if (!diagram->isLoaded() && diagram->keptText().isEmpty())
|
if (!diagram->isLoaded() && diagram->keptText().isEmpty())
|
||||||
m_kept_folios_trusted = false;
|
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_kept_folios_stale = false;
|
||||||
m_edited_since_open = 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))
|
for (Diagram *diagram : std::as_const(m_diagrams_list))
|
||||||
diagram->rememberHeader();
|
diagram->rememberHeader();
|
||||||
if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS_TRACE") == 1)
|
if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS_TRACE") == 1)
|
||||||
@@ -3090,6 +3411,15 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
|
|||||||
m_state = Ok;
|
m_state = Ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QETProject::lazyFolios
|
||||||
|
@see the declaration
|
||||||
|
*/
|
||||||
|
bool QETProject::lazyFolios()
|
||||||
|
{
|
||||||
|
return qEnvironmentVariableIntValue("QET_EAGER_FOLIOS") != 1;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief QETProject::readDiagramsXml
|
@brief QETProject::readDiagramsXml
|
||||||
Load the diagrams from the xml description of the project.
|
Load the diagrams from the xml description of the project.
|
||||||
@@ -3134,7 +3464,7 @@ void QETProject::readDiagramsXml(QDomDocument &xml_project)
|
|||||||
//Where each line of the file starts, for a folio's text: lines
|
//Where each line of the file starts, for a folio's text: lines
|
||||||
//[its start, the next element's start) (QETProject::toXml())
|
//[its start, the next element's start) (QETProject::toXml())
|
||||||
QList<qsizetype> line_starts;
|
QList<qsizetype> line_starts;
|
||||||
if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS") == 1 && !m_opened_content.isEmpty()) {
|
if (m_lazy_open && !m_opened_content.isEmpty()) {
|
||||||
line_starts << 0;
|
line_starts << 0;
|
||||||
for (qsizetype i = 0 ; i < m_opened_content.size() ; ++i)
|
for (qsizetype i = 0 ; i < m_opened_content.size() ; ++i)
|
||||||
if (m_opened_content.at(i) == '\n') line_starts << i + 1;
|
if (m_opened_content.at(i) == '\n') line_starts << i + 1;
|
||||||
@@ -3181,11 +3511,13 @@ void QETProject::readDiagramsXml(QDomDocument &xml_project)
|
|||||||
connect(diagram, &Diagram::usedTitleBlockTemplateChanged,
|
connect(diagram, &Diagram::usedTitleBlockTemplateChanged,
|
||||||
this, &QETProject::usedTitleBlockTemplateChanged);
|
this, &QETProject::usedTitleBlockTemplateChanged);
|
||||||
|
|
||||||
m_deferring_folios = qEnvironmentVariableIntValue("QET_LAZY_FOLIOS") == 1;
|
m_deferring_folios = m_lazy_open;
|
||||||
diagram->initFromXml(diagram_xml_element);
|
diagram->initFromXml(diagram_xml_element);
|
||||||
m_deferring_folios = false;
|
m_deferring_folios = false;
|
||||||
if (!diagram->isLoaded() && !line_starts.isEmpty())
|
if (!diagram->isLoaded() && !line_starts.isEmpty())
|
||||||
diagram->setKeptText(folio_text(diagram_xml_element));
|
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);
|
m_data_base.placeFolio(diagram);
|
||||||
if(dlgWaiting)
|
if(dlgWaiting)
|
||||||
dlgWaiting->setDetail(diagram->title());
|
dlgWaiting->setDetail(diagram->title());
|
||||||
|
|||||||
+48
-4
@@ -32,6 +32,7 @@
|
|||||||
#include "wiringrules.h"
|
#include "wiringrules.h"
|
||||||
#include "diagram.h"
|
#include "diagram.h"
|
||||||
#include "autoNum/assignvariables.h"
|
#include "autoNum/assignvariables.h"
|
||||||
|
#include "container/qetcontainerdb.h"
|
||||||
#ifdef BUILD_WITHOUT_KF
|
#ifdef BUILD_WITHOUT_KF
|
||||||
# include "ui/nokde/kautosavefile.h"
|
# include "ui/nokde/kautosavefile.h"
|
||||||
#else
|
#else
|
||||||
@@ -45,6 +46,7 @@
|
|||||||
#include <QFuture>
|
#include <QFuture>
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
class Diagram;
|
class Diagram;
|
||||||
@@ -119,10 +121,11 @@ class QETProject : public QObject
|
|||||||
projectDataBase *dataBase();
|
projectDataBase *dataBase();
|
||||||
QUuid uuid() const;
|
QUuid uuid() const;
|
||||||
QUuid derivedItemUuid(const QString &kind, const QString &key);
|
QUuid derivedItemUuid(const QString &kind, const QString &key);
|
||||||
|
QByteArray containerPicture(const QString &file) const;
|
||||||
ProjectState state() const;
|
ProjectState state() const;
|
||||||
QList<Diagram *> diagrams() const;
|
QList<Diagram *> diagrams() const;
|
||||||
//The folios whether their items are built or not
|
//The folios whether their items are built or not
|
||||||
//(QET_LAZY_FOLIOS, LAZY-FOLIO-PLAN.md); diagrams() builds them
|
//(lazy folios); diagrams() builds them
|
||||||
QList<Diagram *> folios() const;
|
QList<Diagram *> folios() const;
|
||||||
bool deferringFolios() const {return m_deferring_folios;}
|
bool deferringFolios() const {return m_deferring_folios;}
|
||||||
int unloadedFolioCount() const;
|
int unloadedFolioCount() const;
|
||||||
@@ -130,9 +133,15 @@ class QETProject : public QObject
|
|||||||
QList<Diagram *> cableLabelFolios();
|
QList<Diagram *> cableLabelFolios();
|
||||||
QList<Diagram *> foliosWithWiresNotReconnected();
|
QList<Diagram *> foliosWithWiresNotReconnected();
|
||||||
void buildFolioToShow(Diagram *folio);
|
void buildFolioToShow(Diagram *folio);
|
||||||
|
void buildPartnerSymbols(const QList<Diagram *> &folios, const QSet<QUuid> &partners);
|
||||||
void buildFoliosHolding(ElementData::Types kinds, bool free_only);
|
void buildFoliosHolding(ElementData::Types kinds, bool free_only);
|
||||||
void buildFoliosHoldingSymbols(const QSet<QUuid> &symbols);
|
void buildFoliosHoldingSymbols(const QSet<QUuid> &symbols);
|
||||||
|
void buildFoliosHoldingTables();
|
||||||
QSet<QUuid> symbolsOnUnbuiltFolios() const;
|
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;
|
QSet<Diagram *> tableChainFolios(Diagram *folio) const;
|
||||||
void loadFolios() const;
|
void loadFolios() const;
|
||||||
void buildFolios(const QList<Diagram *> &folios) const;
|
void buildFolios(const QList<Diagram *> &folios) const;
|
||||||
@@ -154,6 +163,12 @@ class QETProject : public QObject
|
|||||||
/// process can destroy the project before the write finishes (crash).
|
/// process can destroy the project before the write finishes (crash).
|
||||||
static void setBackupEnabled(bool enabled);
|
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
|
/// Number of crash-recovery snapshots kept per project, written in
|
||||||
/// turn by writeBackup(), so one bad write cannot replace the only copy
|
/// turn by writeBackup(), so one bad write cannot replace the only copy
|
||||||
static constexpr int BackupGenerations = 3;
|
static constexpr int BackupGenerations = 3;
|
||||||
@@ -259,7 +274,7 @@ class QETProject : public QObject
|
|||||||
static QString normalizedAutoNumName(const QString &name);
|
static QString normalizedAutoNumName(const QString &name);
|
||||||
QVector<Element *> elementsUsingElementAutoNum(const QString &title) const;
|
QVector<Element *> elementsUsingElementAutoNum(const QString &title) const;
|
||||||
/// A placed symbol's numbering, whether its folio is built or
|
/// 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
|
/// project when one is numbered
|
||||||
struct SymbolNumbering {
|
struct SymbolNumbering {
|
||||||
const Element *element = nullptr; ///< null on a folio not built
|
const Element *element = nullptr; ///< null on a folio not built
|
||||||
@@ -313,11 +328,14 @@ class QETProject : public QObject
|
|||||||
void autoFolioNumberingNewFolios ();
|
void autoFolioNumberingNewFolios ();
|
||||||
void autoFolioNumberingSelectedFolios(int, int, const QString&);
|
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);
|
static QString spliceKeptFolios(QString text, const QHash<int, QByteArray> &kept_folios);
|
||||||
bool close();
|
bool close();
|
||||||
QETResult write();
|
QETResult write();
|
||||||
bool isReadOnly() const;
|
bool isReadOnly() const;
|
||||||
|
/// Opened from a .qetz of a later format (opened read-only)
|
||||||
|
bool isFromLaterVersion() const {return m_from_later_version;}
|
||||||
void setReadOnly(bool);
|
void setReadOnly(bool);
|
||||||
bool isEmpty() const;
|
bool isEmpty() const;
|
||||||
ElementsLocation importElement(ElementsLocation &location);
|
ElementsLocation importElement(ElementsLocation &location);
|
||||||
@@ -325,6 +343,7 @@ class QETProject : public QObject
|
|||||||
bool usesElement(const ElementsLocation &) const;
|
bool usesElement(const ElementsLocation &) const;
|
||||||
QList <ElementsLocation> unusedElements() const;
|
QList <ElementsLocation> unusedElements() const;
|
||||||
QList<ElementsLocation> unbuiltFolioElements() const;
|
QList<ElementsLocation> unbuiltFolioElements() const;
|
||||||
|
bool unbuiltFolioHasTables(Diagram *folio) const;
|
||||||
bool usesTitleBlockTemplate(const TitleBlockTemplateLocation &);
|
bool usesTitleBlockTemplate(const TitleBlockTemplateLocation &);
|
||||||
bool projectWasModified();
|
bool projectWasModified();
|
||||||
bool projectOptionsWereModified();
|
bool projectOptionsWereModified();
|
||||||
@@ -436,8 +455,16 @@ class QETProject : public QObject
|
|||||||
ProjectState openFile(QFile *file);
|
ProjectState openFile(QFile *file);
|
||||||
public:
|
public:
|
||||||
static bool isContainerPath(const QString &path);
|
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,
|
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:
|
private:
|
||||||
static QUuid derivedUuid(const QByteArray &content);
|
static QUuid derivedUuid(const QByteArray &content);
|
||||||
void refresh();
|
void refresh();
|
||||||
@@ -448,6 +475,9 @@ class QETProject : public QObject
|
|||||||
static bool m_backup_enabled;
|
static bool m_backup_enabled;
|
||||||
/// Something changed since the last backup, see writeBackup()
|
/// Something changed since the last backup, see writeBackup()
|
||||||
bool m_backup_needed = true;
|
bool m_backup_needed = true;
|
||||||
|
/// The pictures of the .qetz this project was opened from, see
|
||||||
|
/// containerPicture()
|
||||||
|
QHash<QString, QByteArray> m_container_pictures;
|
||||||
/// File path this project is saved to
|
/// File path this project is saved to
|
||||||
QString m_file_path;
|
QString m_file_path;
|
||||||
/// Current state of the project
|
/// Current state of the project
|
||||||
@@ -462,6 +492,7 @@ class QETProject : public QObject
|
|||||||
bool m_modified = false;
|
bool m_modified = false;
|
||||||
/// Whether the project is read only
|
/// Whether the project is read only
|
||||||
bool m_read_only = false;
|
bool m_read_only = false;
|
||||||
|
bool m_from_later_version = false;
|
||||||
/// Filepath for which this project is considered read only
|
/// Filepath for which this project is considered read only
|
||||||
QString read_only_file_path_;
|
QString read_only_file_path_;
|
||||||
/// Default dimensions and properties for new diagrams created within the project
|
/// Default dimensions and properties for new diagrams created within the project
|
||||||
@@ -512,11 +543,24 @@ class QETProject : public QObject
|
|||||||
const DiagramContext &information,
|
const DiagramContext &information,
|
||||||
const QHash<QString, QStringList> &titles_by_formula) const;
|
const QHash<QString, QStringList> &titles_by_formula) const;
|
||||||
bool m_deferring_folios = false;
|
bool m_deferring_folios = false;
|
||||||
|
/// Opening with folios built only when needed: QET_LAZY_FOLIOS
|
||||||
|
/// is set and the database can be filled from the document
|
||||||
|
bool m_lazy_open = false;
|
||||||
/// Saving may write a folio not built as it was read
|
/// Saving may write a folio not built as it was read
|
||||||
/// (writesKeptFolios()): the file is this version's, its
|
/// (writesKeptFolios()): the file is this version's, its
|
||||||
/// database rows came from it, and nothing project-wide that
|
/// database rows came from it, and nothing project-wide that
|
||||||
/// a folio's saved items depend on has changed since
|
/// a folio's saved items depend on has changed since
|
||||||
bool m_kept_folios_trusted = false;
|
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;
|
bool m_kept_folios_stale = false;
|
||||||
/// Something was edited since the project was opened: a table
|
/// Something was edited since the project was opened: a table
|
||||||
/// filled from the database may have changed
|
/// 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())
|
if (raw_path.trimmed().isEmpty() || info.isRelative())
|
||||||
return failure(QStringLiteral("path must be absolute"));
|
return failure(QStringLiteral("path must be absolute"));
|
||||||
if (info.exists()) return failure(QStringLiteral("%1 already exists").arg(raw_path));
|
if (info.exists()) return failure(QStringLiteral("%1 already exists").arg(raw_path));
|
||||||
QDomDocument xml(project->toXml());
|
|
||||||
QString error;
|
QString error;
|
||||||
if (QETProject::isContainerPath(info.absoluteFilePath())
|
if (QETProject::isContainerPath(info.absoluteFilePath())
|
||||||
? !QETProject::writeContainer(xml, info.absoluteFilePath(), &error)
|
? !project->writeContainerFile(info.absoluteFilePath(), &error)
|
||||||
: !QET::writeXmlFile(xml, info.absoluteFilePath(), &error))
|
: ![&]() { QDomDocument xml(project->toXml());
|
||||||
|
return QET::writeXmlFile(xml, info.absoluteFilePath(), &error); }())
|
||||||
return failure(QStringLiteral("snapshot not written: %1").arg(error));
|
return failure(QStringLiteral("snapshot not written: %1").arg(error));
|
||||||
return {{QStringLiteral("ok"), true}, {QStringLiteral("path"), info.absoluteFilePath()},
|
return {{QStringLiteral("ok"), true}, {QStringLiteral("path"), info.absoluteFilePath()},
|
||||||
{QStringLiteral("folio"), e->currentDiagramView()
|
{QStringLiteral("folio"), e->currentDiagramView()
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ QString QetScriptApi::filePath() const
|
|||||||
|
|
||||||
int QetScriptApi::folioCount() 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
|
int QetScriptApi::currentFolio() const
|
||||||
{
|
{
|
||||||
if (!m_project) return -1;
|
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()) {
|
if (m_view && m_view->diagram()) {
|
||||||
return diagrams.indexOf(m_view->diagram());
|
return diagrams.indexOf(m_view->diagram());
|
||||||
}
|
}
|
||||||
@@ -193,7 +193,7 @@ int QetScriptApi::currentFolio() const
|
|||||||
QString QetScriptApi::folioTitle(int index) const
|
QString QetScriptApi::folioTitle(int index) const
|
||||||
{
|
{
|
||||||
if (!m_project) return QString();
|
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();
|
if (index < 0 || index >= diagrams.count()) return QString();
|
||||||
return diagrams.at(index)->title();
|
return diagrams.at(index)->title();
|
||||||
}
|
}
|
||||||
@@ -207,7 +207,7 @@ QString QetScriptApi::folioTitle(int index) const
|
|||||||
QString QetScriptApi::folioUuid(int index) const
|
QString QetScriptApi::folioUuid(int index) const
|
||||||
{
|
{
|
||||||
if (!m_project) return QString();
|
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();
|
if (index < 0 || index >= diagrams.count()) return QString();
|
||||||
return diagrams.at(index)->uuid().toString();
|
return diagrams.at(index)->uuid().toString();
|
||||||
}
|
}
|
||||||
@@ -222,7 +222,7 @@ int QetScriptApi::folioIndex(const QString &uuid) const
|
|||||||
if (!m_project) return -1;
|
if (!m_project) return -1;
|
||||||
const QUuid wanted(uuid);
|
const QUuid wanted(uuid);
|
||||||
if (wanted.isNull()) return -1;
|
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) {
|
for (int i = 0 ; i < diagrams.count() ; ++i) {
|
||||||
if (diagrams.at(i)->uuid() == wanted) return 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
|
int QetScriptApi::elementCount(int folioIndex) const
|
||||||
{
|
{
|
||||||
if (!m_project) return 0;
|
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;
|
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();
|
return content.m_elements.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
int QetScriptApi::conductorCount(int folioIndex) const
|
int QetScriptApi::conductorCount(int folioIndex) const
|
||||||
{
|
{
|
||||||
if (!m_project) return 0;
|
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;
|
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();
|
return content.conductors(DiagramContent::AnyConductor).count();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,11 +434,11 @@ bool QetScriptApi::save(const QString &output)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
QDomDocument xml_doc(m_project->toXml());
|
|
||||||
QString error_message;
|
QString error_message;
|
||||||
if (QETProject::isContainerPath(output)
|
if (QETProject::isContainerPath(output)
|
||||||
? !QETProject::writeContainer(xml_doc, output, &error_message)
|
? !m_project->writeContainerFile(output, &error_message)
|
||||||
: !QET::writeXmlFile(xml_doc, 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));
|
log(QStringLiteral("qet.save: %1").arg(error_message));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -462,6 +462,41 @@ void QetScriptApi::setLive(QStringList *log)
|
|||||||
m_live_log = 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
|
@brief QetScriptApi::findElement
|
||||||
@param folioIndex
|
@param folioIndex
|
||||||
@@ -472,9 +507,9 @@ void QetScriptApi::setLive(QStringList *log)
|
|||||||
Element *QetScriptApi::findElement(int folioIndex, const QString &elementUuid) const
|
Element *QetScriptApi::findElement(int folioIndex, const QString &elementUuid) const
|
||||||
{
|
{
|
||||||
if (!m_project) return nullptr;
|
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;
|
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)) {
|
for (Element *elmt : std::as_const(content.m_elements)) {
|
||||||
if (elmt->uuid().toString() == elementUuid)
|
if (elmt->uuid().toString() == elementUuid)
|
||||||
return elmt;
|
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()")
|
log(QStringLiteral("qet.%1: \"%2\" is internal, follow a numbering with numberElement()")
|
||||||
.arg(caller, key));
|
.arg(caller, key));
|
||||||
return false;
|
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);
|
Element *element = findElement(folioIndex, elementUuid);
|
||||||
if (!element) return false;
|
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"));
|
log(QStringLiteral("qet.addElement: project is read-only"));
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
const QList<Diagram *> diagrams = m_project->diagrams();
|
const QList<Diagram *> diagrams = m_project->folios();
|
||||||
if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString();
|
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
|
// ElementsLocation::setPath() forces ANY path to embed:// as soon as a
|
||||||
// non-null project is given, common://custom:// included -- passing
|
// 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);
|
Element *element = findElement(folioIndex, elementUuid);
|
||||||
if (!element) return false;
|
if (!element) return false;
|
||||||
Diagram *diagram = m_project->diagrams().at(folioIndex);
|
Diagram *diagram = built(m_project->folios().at(folioIndex));
|
||||||
|
|
||||||
DiagramContent content;
|
DiagramContent content;
|
||||||
content.m_elements << element;
|
content.m_elements << element;
|
||||||
@@ -906,9 +954,9 @@ QStringList QetScriptApi::elementUuids(int folioIndex) const
|
|||||||
{
|
{
|
||||||
QStringList uuids;
|
QStringList uuids;
|
||||||
if (!m_project) return 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;
|
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)) {
|
for (Element *elmt : std::as_const(content.m_elements)) {
|
||||||
uuids << elmt->uuid().toString();
|
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
|
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);
|
Element *element = findElement(folioIndex, elementUuid);
|
||||||
if (!element) return QString();
|
if (!element) return QString();
|
||||||
return element->elementInformations().value(key).toString();
|
return element->elementInformations().value(key).toString();
|
||||||
@@ -1077,7 +1127,7 @@ bool QetScriptApi::addConductor(int folioIndex,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Diagram *diagram = m_project->diagrams().at(folioIndex);
|
Diagram *diagram = built(m_project->folios().at(folioIndex));
|
||||||
ConductorCreator creator(diagram, terminals);
|
ConductorCreator creator(diagram, terminals);
|
||||||
Q_UNUSED(creator)
|
Q_UNUSED(creator)
|
||||||
|
|
||||||
@@ -1110,10 +1160,10 @@ QStringList QetScriptApi::conductors(int folioIndex) const
|
|||||||
{
|
{
|
||||||
QStringList list;
|
QStringList list;
|
||||||
if (!m_project) return 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;
|
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);
|
const QList<Conductor *> all = content.conductors(DiagramContent::AnyConductor);
|
||||||
for (Conductor *c : all)
|
for (Conductor *c : all)
|
||||||
{
|
{
|
||||||
@@ -1132,10 +1182,10 @@ QStringList QetScriptApi::conductorUuids(int folioIndex) const
|
|||||||
{
|
{
|
||||||
QStringList list;
|
QStringList list;
|
||||||
if (!m_project) return 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;
|
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))
|
for (Conductor *c : content.conductors(DiagramContent::AnyConductor))
|
||||||
list << c->uuid().toString();
|
list << c->uuid().toString();
|
||||||
return list;
|
return list;
|
||||||
@@ -1152,12 +1202,12 @@ QStringList QetScriptApi::conductorUuids(int folioIndex) const
|
|||||||
QStringList QetScriptApi::conductorEnds(int folioIndex, const QString &uuid) const
|
QStringList QetScriptApi::conductorEnds(int folioIndex, const QString &uuid) const
|
||||||
{
|
{
|
||||||
if (!m_project) return {};
|
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 {};
|
if (folioIndex < 0 || folioIndex >= diagrams.count()) return {};
|
||||||
const QUuid wanted(uuid);
|
const QUuid wanted(uuid);
|
||||||
if (wanted.isNull()) return {};
|
if (wanted.isNull()) return {};
|
||||||
|
|
||||||
DiagramContent content(diagrams.at(folioIndex), false);
|
DiagramContent content(built(diagrams.at(folioIndex)), false);
|
||||||
for (Conductor *c : content.conductors(DiagramContent::AnyConductor))
|
for (Conductor *c : content.conductors(DiagramContent::AnyConductor))
|
||||||
if (c->uuid() == wanted)
|
if (c->uuid() == wanted)
|
||||||
return {describeEnd(c->terminal1), describeEnd(c->terminal2)};
|
return {describeEnd(c->terminal1), describeEnd(c->terminal2)};
|
||||||
@@ -1189,6 +1239,10 @@ QString QetScriptApi::conductorProperty(int folioIndex, const QString &elementUu
|
|||||||
describes a potential and not one drawn segment. Setting it on one
|
describes a potential and not one drawn segment. Setting it on one
|
||||||
conductor and leaving the rest of the potential disagreeing would
|
conductor and leaving the rest of the potential disagreeing would
|
||||||
produce a file no GUI action could have produced.
|
produce a file no GUI action could have produced.
|
||||||
|
|
||||||
|
One thing stops at a terminal element with its function separation
|
||||||
|
enabled: every property but the voltage / protocol, which still
|
||||||
|
crosses it (@see Conductor::functionSeparatedConductors).
|
||||||
@return true if anything was changed, or if it already held that value
|
@return true if anything was changed, or if it already held that value
|
||||||
*/
|
*/
|
||||||
bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUuid,
|
bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUuid,
|
||||||
@@ -1215,7 +1269,21 @@ bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUu
|
|||||||
.arg(caller, value, property));
|
.arg(caller, value, property));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (properties == conductor->properties()) return true; // already so
|
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);
|
QSet<Conductor *> potential = conductor->relatedPotentialConductors(true);
|
||||||
potential << conductor;
|
potential << conductor;
|
||||||
@@ -1227,11 +1295,198 @@ bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUu
|
|||||||
old_value.setValue(c->properties());
|
old_value.setValue(c->properties());
|
||||||
new_value.setValue(properties);
|
new_value.setValue(properties);
|
||||||
m_project->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value));
|
m_project->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value));
|
||||||
|
}
|
||||||
|
//The other side of a function separated terminal element gets the
|
||||||
|
//voltage / protocol only: it is the one property that still
|
||||||
|
//describes the electrical node rather than one side of it. And it
|
||||||
|
//only arrives when this call really changed it -- before / after
|
||||||
|
//is the edit, not the far wire's own value, which differs from it
|
||||||
|
//just as often as not and would then be overwritten by every call,
|
||||||
|
//colour included.
|
||||||
|
for (Conductor *c : conductor->functionSeparatedConductors(true))
|
||||||
|
{
|
||||||
|
ConductorProperties separated_properties = c->properties();
|
||||||
|
separated_properties.applyTensionProtocolChange(before, properties);
|
||||||
|
if (separated_properties == c->properties())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
QVariant old_value, new_value;
|
||||||
|
old_value.setValue(c->properties());
|
||||||
|
new_value.setValue(separated_properties);
|
||||||
|
m_project->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value));
|
||||||
}
|
}
|
||||||
m_project->undoStack()->endMacro();
|
m_project->undoStack()->endMacro();
|
||||||
return true;
|
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
|
@brief QetScriptApi::conductorSegments
|
||||||
List the drawn path of the conductor on a terminal, one line per
|
List the drawn path of the conductor on a terminal, one line per
|
||||||
@@ -1787,18 +2042,18 @@ QList<T *> sortedByPosition(const QSet<T *> &items)
|
|||||||
QList<IndependentTextItem *> QetScriptApi::sortedTexts(int folioIndex) const
|
QList<IndependentTextItem *> QetScriptApi::sortedTexts(int folioIndex) const
|
||||||
{
|
{
|
||||||
if (!m_project) return {};
|
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 {};
|
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);
|
return sortedByPosition(content.m_text_fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QetShapeItem *> QetScriptApi::sortedShapes(int folioIndex) const
|
QList<QetShapeItem *> QetScriptApi::sortedShapes(int folioIndex) const
|
||||||
{
|
{
|
||||||
if (!m_project) return {};
|
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 {};
|
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);
|
return sortedByPosition(content.m_shapes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1843,9 +2098,9 @@ int QetScriptApi::addText(int folioIndex, const QString &text, double x, double
|
|||||||
log(QStringLiteral("qet.addText: project is read-only"));
|
log(QStringLiteral("qet.addText: project is read-only"));
|
||||||
return -1;
|
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 (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
|
||||||
Diagram *diagram = diagrams.at(folioIndex);
|
Diagram *diagram = built(diagrams.at(folioIndex));
|
||||||
|
|
||||||
auto *item = new IndependentTextItem();
|
auto *item = new IndependentTextItem();
|
||||||
item->setPlainText(text);
|
item->setPlainText(text);
|
||||||
@@ -1929,7 +2184,7 @@ bool QetScriptApi::deleteText(int folioIndex, int textIndex)
|
|||||||
}
|
}
|
||||||
IndependentTextItem *item = findText(folioIndex, textIndex, QStringLiteral("deleteText"));
|
IndependentTextItem *item = findText(folioIndex, textIndex, QStringLiteral("deleteText"));
|
||||||
if (!item) return false;
|
if (!item) return false;
|
||||||
Diagram *diagram = m_project->diagrams().at(folioIndex);
|
Diagram *diagram = built(m_project->folios().at(folioIndex));
|
||||||
|
|
||||||
DiagramContent content;
|
DiagramContent content;
|
||||||
content.m_text_fields << item;
|
content.m_text_fields << item;
|
||||||
@@ -1969,7 +2224,7 @@ int QetScriptApi::addShape(int folioIndex, const QString &type,
|
|||||||
log(QStringLiteral("qet.addShape: project is read-only"));
|
log(QStringLiteral("qet.addShape: project is read-only"));
|
||||||
return -1;
|
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 (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
|
||||||
|
|
||||||
QetShapeItem::ShapeType shape_type;
|
QetShapeItem::ShapeType shape_type;
|
||||||
@@ -1984,7 +2239,7 @@ int QetScriptApi::addShape(int folioIndex, const QString &type,
|
|||||||
return -1;
|
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);
|
auto *shape = new QetShapeItem(QPointF(x1, y1), QPointF(x2, y2), shape_type);
|
||||||
diagram->undoStack().push(new AddGraphicsObjectCommand(shape, diagram, QPointF(0, 0)));
|
diagram->undoStack().push(new AddGraphicsObjectCommand(shape, diagram, QPointF(0, 0)));
|
||||||
return sortedShapes(folioIndex).indexOf(shape);
|
return sortedShapes(folioIndex).indexOf(shape);
|
||||||
@@ -2116,7 +2371,7 @@ bool QetScriptApi::deleteShape(int folioIndex, int shapeIndex)
|
|||||||
.arg(folioIndex).arg(list.count()).arg(shapeIndex));
|
.arg(folioIndex).arg(list.count()).arg(shapeIndex));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Diagram *diagram = m_project->diagrams().at(folioIndex);
|
Diagram *diagram = built(m_project->folios().at(folioIndex));
|
||||||
|
|
||||||
DiagramContent content;
|
DiagramContent content;
|
||||||
content.m_shapes << list.at(shapeIndex);
|
content.m_shapes << list.at(shapeIndex);
|
||||||
@@ -2223,7 +2478,7 @@ int QetScriptApi::addPolygon(int folioIndex, const QVariantList &points, bool cl
|
|||||||
log(QStringLiteral("qet.addPolygon: project is read-only"));
|
log(QStringLiteral("qet.addPolygon: project is read-only"));
|
||||||
return -1;
|
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 (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
|
||||||
if (points.count() < 2) {
|
if (points.count() < 2) {
|
||||||
log(QStringLiteral("qet.addPolygon: at least 2 points are required, got %1")
|
log(QStringLiteral("qet.addPolygon: at least 2 points are required, got %1")
|
||||||
@@ -2237,7 +2492,7 @@ int QetScriptApi::addPolygon(int folioIndex, const QVariantList &points, bool cl
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Diagram *diagram = diagrams.at(folioIndex);
|
Diagram *diagram = built(diagrams.at(folioIndex));
|
||||||
auto *shape = new QetShapeItem(poly.first(), poly.last(), QetShapeItem::Polygon);
|
auto *shape = new QetShapeItem(poly.first(), poly.last(), QetShapeItem::Polygon);
|
||||||
shape->setPolygon(poly);
|
shape->setPolygon(poly);
|
||||||
shape->setClosed(closed);
|
shape->setClosed(closed);
|
||||||
@@ -2321,7 +2576,7 @@ int QetScriptApi::addPath(int folioIndex, const QVariantList &nodes, bool closed
|
|||||||
log(QStringLiteral("qet.addPath: project is read-only"));
|
log(QStringLiteral("qet.addPath: project is read-only"));
|
||||||
return -1;
|
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 (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
|
||||||
if (nodes.count() < 2) {
|
if (nodes.count() < 2) {
|
||||||
log(QStringLiteral("qet.addPath: at least 2 nodes are required, got %1").arg(nodes.count()));
|
log(QStringLiteral("qet.addPath: at least 2 nodes are required, got %1").arg(nodes.count()));
|
||||||
@@ -2338,7 +2593,7 @@ int QetScriptApi::addPath(int folioIndex, const QVariantList &nodes, bool closed
|
|||||||
path_nodes << node;
|
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,
|
auto *shape = new QetShapeItem(path_nodes.first().anchor, path_nodes.last().anchor,
|
||||||
QetShapeItem::Path);
|
QetShapeItem::Path);
|
||||||
shape->setPathNodes(path_nodes);
|
shape->setPathNodes(path_nodes);
|
||||||
@@ -2582,7 +2837,7 @@ bool QetScriptApi::deleteConductor(int folioIndex, const QString &elementUuid, i
|
|||||||
}
|
}
|
||||||
Conductor *conductor = findConductor(folioIndex, elementUuid, terminalIndex, caller);
|
Conductor *conductor = findConductor(folioIndex, elementUuid, terminalIndex, caller);
|
||||||
if (!conductor) return false;
|
if (!conductor) return false;
|
||||||
Diagram *diagram = m_project->diagrams().at(folioIndex);
|
Diagram *diagram = built(m_project->folios().at(folioIndex));
|
||||||
|
|
||||||
DiagramContent content;
|
DiagramContent content;
|
||||||
content.m_conductors_to_move << conductor;
|
content.m_conductors_to_move << conductor;
|
||||||
@@ -2603,10 +2858,10 @@ bool QetScriptApi::removeFolio(int folioIndex)
|
|||||||
log(QStringLiteral("qet.removeFolio: project is read-only"));
|
log(QStringLiteral("qet.removeFolio: project is read-only"));
|
||||||
return false;
|
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 (folioIndex < 0 || folioIndex >= diagrams.count()) return false;
|
||||||
m_project->undoStack()->push(new RemoveDiagramCommand(m_project, diagrams.at(folioIndex)));
|
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 {
|
namespace {
|
||||||
@@ -2640,7 +2895,7 @@ const QStringList &titleBlockFieldNames()
|
|||||||
QString QetScriptApi::folioProperty(int folioIndex, const QString &property) const
|
QString QetScriptApi::folioProperty(int folioIndex, const QString &property) const
|
||||||
{
|
{
|
||||||
if (!m_project) return QString();
|
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();
|
if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString();
|
||||||
Diagram *diagram = diagrams.at(folioIndex);
|
Diagram *diagram = diagrams.at(folioIndex);
|
||||||
if (property == QLatin1String("template"))
|
if (property == QLatin1String("template"))
|
||||||
@@ -2665,7 +2920,7 @@ bool QetScriptApi::setFolioProperty(int folioIndex, const QString &property, con
|
|||||||
log(QStringLiteral("qet.setFolioProperty: project is read-only"));
|
log(QStringLiteral("qet.setFolioProperty: project is read-only"));
|
||||||
return false;
|
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 (folioIndex < 0 || folioIndex >= diagrams.count()) return false;
|
||||||
Diagram *diagram = diagrams.at(folioIndex);
|
Diagram *diagram = diagrams.at(folioIndex);
|
||||||
|
|
||||||
@@ -2958,9 +3213,9 @@ bool QetScriptApi::sortTerminalStrip(int stripIndex)
|
|||||||
QList<QetGraphicsTableItem *> QetScriptApi::sortedTables(int folioIndex) const
|
QList<QetGraphicsTableItem *> QetScriptApi::sortedTables(int folioIndex) const
|
||||||
{
|
{
|
||||||
if (!m_project) return {};
|
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 {};
|
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);
|
return sortedByPosition(content.m_tables);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3020,7 +3275,7 @@ int QetScriptApi::addTable(int folioIndex, const QString &kind, const QString &n
|
|||||||
log(QStringLiteral("qet.%1: project is read-only").arg(caller));
|
log(QStringLiteral("qet.%1: project is read-only").arg(caller));
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
const QList<Diagram *> diagrams = m_project->diagrams();
|
const QList<Diagram *> diagrams = m_project->folios();
|
||||||
if (folioIndex < 0 || folioIndex >= diagrams.count()) {
|
if (folioIndex < 0 || folioIndex >= diagrams.count()) {
|
||||||
log(QStringLiteral("qet.%1: no folio at index %2").arg(caller).arg(folioIndex));
|
log(QStringLiteral("qet.%1: no folio at index %2").arg(caller).arg(folioIndex));
|
||||||
return -1;
|
return -1;
|
||||||
@@ -3089,7 +3344,7 @@ int QetScriptApi::addTable(int folioIndex, const QString &kind, const QString &n
|
|||||||
dialog.setAddNewTableToNewDiagram(false);
|
dialog.setAddNewTableToNewDiagram(false);
|
||||||
|
|
||||||
const QList<QetGraphicsTableItem *> before = sortedTables(folioIndex);
|
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);
|
const QList<QetGraphicsTableItem *> after = sortedTables(folioIndex);
|
||||||
for (QetGraphicsTableItem *t : after) {
|
for (QetGraphicsTableItem *t : after) {
|
||||||
if (!before.contains(t)) return after.indexOf(t);
|
if (!before.contains(t)) return after.indexOf(t);
|
||||||
@@ -3105,9 +3360,9 @@ bool QetScriptApi::deleteTable(int folioIndex, int tableIndex)
|
|||||||
log(QStringLiteral("qet.deleteTable: project is read-only"));
|
log(QStringLiteral("qet.deleteTable: project is read-only"));
|
||||||
return false;
|
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 (folioIndex < 0 || folioIndex >= diagrams.count()) return false;
|
||||||
Diagram *diagram = diagrams.at(folioIndex);
|
Diagram *diagram = built(diagrams.at(folioIndex));
|
||||||
|
|
||||||
const QList<QetGraphicsTableItem *> all = sortedTables(folioIndex);
|
const QList<QetGraphicsTableItem *> all = sortedTables(folioIndex);
|
||||||
if (tableIndex < 0 || tableIndex >= all.count()) {
|
if (tableIndex < 0 || tableIndex >= all.count()) {
|
||||||
@@ -3381,7 +3636,7 @@ bool QetScriptApi::useConductorAutoNum(int folioIndex, const QString &name)
|
|||||||
log(QStringLiteral("qet.useConductorAutoNum: project is read-only"));
|
log(QStringLiteral("qet.useConductorAutoNum: project is read-only"));
|
||||||
return false;
|
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 (folioIndex < 0 || folioIndex >= diagrams.count()) return false;
|
||||||
if (!name.isEmpty() && !m_project->conductorAutoNum().contains(name)) {
|
if (!name.isEmpty() && !m_project->conductorAutoNum().contains(name)) {
|
||||||
log(QStringLiteral("qet.useConductorAutoNum: no conductor auto-numbering named '%1'").arg(name));
|
log(QStringLiteral("qet.useConductorAutoNum: no conductor auto-numbering named '%1'").arg(name));
|
||||||
@@ -3395,9 +3650,9 @@ bool QetScriptApi::useConductorAutoNum(int folioIndex, const QString &name)
|
|||||||
QList<DiagramImageItem *> QetScriptApi::sortedImages(int folioIndex) const
|
QList<DiagramImageItem *> QetScriptApi::sortedImages(int folioIndex) const
|
||||||
{
|
{
|
||||||
if (!m_project) return {};
|
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 {};
|
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);
|
return sortedByPosition(content.m_images);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3491,7 +3746,7 @@ int QetScriptApi::addImage(int folioIndex, const QString &filePath, double x, do
|
|||||||
log(QStringLiteral("qet.addImage: project is read-only"));
|
log(QStringLiteral("qet.addImage: project is read-only"));
|
||||||
return -1;
|
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 (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
|
||||||
|
|
||||||
// Same checks as a picture dropped on a folio: a regular file of at
|
// Same checks as a picture dropped on a folio: a regular file of at
|
||||||
@@ -3503,7 +3758,7 @@ int QetScriptApi::addImage(int folioIndex, const QString &filePath, double x, do
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Diagram *diagram = diagrams.at(folioIndex);
|
Diagram *diagram = built(diagrams.at(folioIndex));
|
||||||
auto *item = new DiagramImageItem(QPixmap::fromImage(image));
|
auto *item = new DiagramImageItem(QPixmap::fromImage(image));
|
||||||
diagram->undoStack().push(new AddGraphicsObjectCommand(item, diagram, QPointF(x, y)));
|
diagram->undoStack().push(new AddGraphicsObjectCommand(item, diagram, QPointF(x, y)));
|
||||||
return sortedImages(folioIndex).indexOf(item);
|
return sortedImages(folioIndex).indexOf(item);
|
||||||
@@ -3619,7 +3874,7 @@ bool QetScriptApi::deleteImage(int folioIndex, int imageIndex)
|
|||||||
.arg(folioIndex).arg(list.count()).arg(imageIndex));
|
.arg(folioIndex).arg(list.count()).arg(imageIndex));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Diagram *diagram = m_project->diagrams().at(folioIndex);
|
Diagram *diagram = built(m_project->folios().at(folioIndex));
|
||||||
DiagramContent content;
|
DiagramContent content;
|
||||||
content.m_images << list.at(imageIndex);
|
content.m_images << list.at(imageIndex);
|
||||||
diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, content));
|
diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, content));
|
||||||
@@ -3675,7 +3930,7 @@ int QetScriptApi::addPdfPage(int folioIndex, const QString &pdfPath, int pageNum
|
|||||||
log(QStringLiteral("qet.addPdfPage: project is read-only"));
|
log(QStringLiteral("qet.addPdfPage: project is read-only"));
|
||||||
return -1;
|
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 (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
|
||||||
if (dpi <= 0) {
|
if (dpi <= 0) {
|
||||||
log(QStringLiteral("qet.addPdfPage: dpi must be positive, got %1").arg(dpi));
|
log(QStringLiteral("qet.addPdfPage: dpi must be positive, got %1").arg(dpi));
|
||||||
@@ -3724,7 +3979,7 @@ int QetScriptApi::addPdfPage(int folioIndex, const QString &pdfPath, int pageNum
|
|||||||
painter.drawImage(0, 0, rendered);
|
painter.drawImage(0, 0, rendered);
|
||||||
painter.end();
|
painter.end();
|
||||||
|
|
||||||
Diagram *diagram = diagrams.at(folioIndex);
|
Diagram *diagram = built(diagrams.at(folioIndex));
|
||||||
auto *item = new DiagramImageItem(QPixmap::fromImage(background));
|
auto *item = new DiagramImageItem(QPixmap::fromImage(background));
|
||||||
diagram->undoStack().push(new AddGraphicsObjectCommand(item, diagram, QPointF(x, y)));
|
diagram->undoStack().push(new AddGraphicsObjectCommand(item, diagram, QPointF(x, y)));
|
||||||
return sortedImages(folioIndex).indexOf(item);
|
return sortedImages(folioIndex).indexOf(item);
|
||||||
@@ -3948,7 +4203,7 @@ bool QetScriptApi::deleteElementText(int folioIndex, const QString &elementUuid,
|
|||||||
DynamicElementTextItem *t = findElementText(folioIndex, elementUuid, textIndex,
|
DynamicElementTextItem *t = findElementText(folioIndex, elementUuid, textIndex,
|
||||||
QStringLiteral("deleteElementText"));
|
QStringLiteral("deleteElementText"));
|
||||||
if (!t) return false;
|
if (!t) return false;
|
||||||
Diagram *diagram = m_project->diagrams().at(folioIndex);
|
Diagram *diagram = built(m_project->folios().at(folioIndex));
|
||||||
DiagramContent content;
|
DiagramContent content;
|
||||||
content.m_element_texts << t;
|
content.m_element_texts << t;
|
||||||
diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, content));
|
diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, content));
|
||||||
@@ -4178,7 +4433,7 @@ QStringList QetScriptApi::duplicateElements(int fromFolioIndex, const QStringLis
|
|||||||
log(QStringLiteral("qet.%1: project is read-only").arg(caller));
|
log(QStringLiteral("qet.%1: project is read-only").arg(caller));
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
const QList<Diagram *> diagrams = m_project->diagrams();
|
const QList<Diagram *> diagrams = m_project->folios();
|
||||||
if (fromFolioIndex < 0 || fromFolioIndex >= diagrams.count()
|
if (fromFolioIndex < 0 || fromFolioIndex >= diagrams.count()
|
||||||
|| toFolioIndex < 0 || toFolioIndex >= diagrams.count()) {
|
|| toFolioIndex < 0 || toFolioIndex >= diagrams.count()) {
|
||||||
log(QStringLiteral("qet.%1: folio index out of range").arg(caller));
|
log(QStringLiteral("qet.%1: folio index out of range").arg(caller));
|
||||||
@@ -4188,8 +4443,8 @@ QStringList QetScriptApi::duplicateElements(int fromFolioIndex, const QStringLis
|
|||||||
log(QStringLiteral("qet.%1: no elements named").arg(caller));
|
log(QStringLiteral("qet.%1: no elements named").arg(caller));
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
Diagram *source = diagrams.at(fromFolioIndex);
|
Diagram *source = built(diagrams.at(fromFolioIndex));
|
||||||
Diagram *target = diagrams.at(toFolioIndex);
|
Diagram *target = built(diagrams.at(toFolioIndex));
|
||||||
|
|
||||||
QList<Element *> chosen;
|
QList<Element *> chosen;
|
||||||
for (const QString &uuid : elementUuids) {
|
for (const QString &uuid : elementUuids) {
|
||||||
@@ -4273,7 +4528,7 @@ const QStringList &folioBorderNames()
|
|||||||
QString QetScriptApi::folioBorder(int folioIndex, const QString &property) const
|
QString QetScriptApi::folioBorder(int folioIndex, const QString &property) const
|
||||||
{
|
{
|
||||||
if (!m_project) return QString();
|
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();
|
if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString();
|
||||||
const BorderProperties b = diagrams.at(folioIndex)->border_and_titleblock.exportBorder();
|
const BorderProperties b = diagrams.at(folioIndex)->border_and_titleblock.exportBorder();
|
||||||
if (property == QLatin1String("columns")) return QString::number(b.columns_count);
|
if (property == QLatin1String("columns")) return QString::number(b.columns_count);
|
||||||
@@ -4412,7 +4667,7 @@ bool QetScriptApi::setFolioBorder(int folioIndex, const QString &property, const
|
|||||||
.arg(caller, property, folioBorderNames().join(QStringLiteral(", "))));
|
.arg(caller, property, folioBorderNames().join(QStringLiteral(", "))));
|
||||||
return false;
|
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 (folioIndex < 0 || folioIndex >= diagrams.count()) return false;
|
||||||
Diagram *diagram = diagrams.at(folioIndex);
|
Diagram *diagram = diagrams.at(folioIndex);
|
||||||
|
|
||||||
@@ -4498,7 +4753,7 @@ QString QetScriptApi::conductorDefault(int folioIndex, const QString &property)
|
|||||||
if (folioIndex == -1) {
|
if (folioIndex == -1) {
|
||||||
p = m_project->defaultConductorProperties();
|
p = m_project->defaultConductorProperties();
|
||||||
} else {
|
} else {
|
||||||
const QList<Diagram *> diagrams = m_project->diagrams();
|
const QList<Diagram *> diagrams = m_project->folios();
|
||||||
if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString();
|
if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString();
|
||||||
p = diagrams.at(folioIndex)->defaultConductorProperties;
|
p = diagrams.at(folioIndex)->defaultConductorProperties;
|
||||||
}
|
}
|
||||||
@@ -4524,7 +4779,7 @@ bool QetScriptApi::setConductorDefault(int folioIndex, const QString &property,
|
|||||||
}
|
}
|
||||||
Diagram *diagram = nullptr;
|
Diagram *diagram = nullptr;
|
||||||
if (folioIndex != -1) {
|
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;
|
if (folioIndex < 0 || folioIndex >= diagrams.count()) return false;
|
||||||
diagram = diagrams.at(folioIndex);
|
diagram = diagrams.at(folioIndex);
|
||||||
}
|
}
|
||||||
@@ -4633,12 +4888,12 @@ QVariantMap QetScriptApi::terminalPosition(int folioIndex, const QString &elemen
|
|||||||
QVariantList QetScriptApi::conductorPath(int folioIndex, const QString &conductorUuid) const
|
QVariantList QetScriptApi::conductorPath(int folioIndex, const QString &conductorUuid) const
|
||||||
{
|
{
|
||||||
if (!m_project) return {};
|
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 {};
|
if (folioIndex < 0 || folioIndex >= diagrams.count()) return {};
|
||||||
const QUuid wanted(conductorUuid);
|
const QUuid wanted(conductorUuid);
|
||||||
if (wanted.isNull()) return {};
|
if (wanted.isNull()) return {};
|
||||||
|
|
||||||
DiagramContent content(diagrams.at(folioIndex), false);
|
DiagramContent content(built(diagrams.at(folioIndex)), false);
|
||||||
for (Conductor *c : content.conductors(DiagramContent::AnyConductor)) {
|
for (Conductor *c : content.conductors(DiagramContent::AnyConductor)) {
|
||||||
if (c->uuid() != wanted) continue;
|
if (c->uuid() != wanted) continue;
|
||||||
QVariantList points;
|
QVariantList points;
|
||||||
@@ -4673,14 +4928,14 @@ int QetScriptApi::insertFolio(int position)
|
|||||||
log(QStringLiteral("qet.insertFolio: project is read-only"));
|
log(QStringLiteral("qet.insertFolio: project is read-only"));
|
||||||
return -1;
|
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")
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
Diagram *diagram = m_project->addNewDiagram(position);
|
Diagram *diagram = m_project->addNewDiagram(position);
|
||||||
if (!diagram) return -1;
|
if (!diagram) return -1;
|
||||||
return m_project->diagrams().indexOf(diagram);
|
return m_project->folios().indexOf(diagram);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -4760,7 +5015,7 @@ int QetScriptApi::addFolio()
|
|||||||
}
|
}
|
||||||
Diagram *diagram = m_project->addNewDiagram();
|
Diagram *diagram = m_project->addNewDiagram();
|
||||||
if (!diagram) return -1;
|
if (!diagram) return -1;
|
||||||
return m_project->diagrams().indexOf(diagram);
|
return m_project->folios().indexOf(diagram);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QetScriptApi::setFolioTitle(int folioIndex, const QString &title)
|
bool QetScriptApi::setFolioTitle(int folioIndex, const QString &title)
|
||||||
@@ -4770,7 +5025,7 @@ bool QetScriptApi::setFolioTitle(int folioIndex, const QString &title)
|
|||||||
log(QStringLiteral("qet.setFolioTitle: project is read-only"));
|
log(QStringLiteral("qet.setFolioTitle: project is read-only"));
|
||||||
return false;
|
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 (folioIndex < 0 || folioIndex >= diagrams.count()) return false;
|
||||||
Diagram *diagram = diagrams.at(folioIndex);
|
Diagram *diagram = diagrams.at(folioIndex);
|
||||||
|
|
||||||
@@ -5202,7 +5457,15 @@ QVariantList QetScriptApi::checkContinuity(int folioIndex)
|
|||||||
bool QetScriptApi::selectElement(const QString &elementUuid)
|
bool QetScriptApi::selectElement(const QString &elementUuid)
|
||||||
{
|
{
|
||||||
if (!m_project) return false;
|
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);
|
DiagramContent content(diagram, false);
|
||||||
for (Element *elmt : std::as_const(content.m_elements)) {
|
for (Element *elmt : std::as_const(content.m_elements)) {
|
||||||
if (elmt->uuid().toString() == elementUuid) {
|
if (elmt->uuid().toString() == elementUuid) {
|
||||||
@@ -5218,7 +5481,7 @@ QStringList QetScriptApi::selectedElements(int folioIndex) const
|
|||||||
{
|
{
|
||||||
QStringList list;
|
QStringList list;
|
||||||
if (!m_project) return 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;
|
if (folioIndex < 0 || folioIndex >= diagrams.count()) return list;
|
||||||
for (QGraphicsItem *item : diagrams.at(folioIndex)->selectedItems()) {
|
for (QGraphicsItem *item : diagrams.at(folioIndex)->selectedItems()) {
|
||||||
if (item->type() == Element::Type) {
|
if (item->type() == Element::Type) {
|
||||||
@@ -5232,7 +5495,7 @@ QStringList QetScriptApi::selectedElements(int folioIndex) const
|
|||||||
void QetScriptApi::deselectAll(int folioIndex)
|
void QetScriptApi::deselectAll(int folioIndex)
|
||||||
{
|
{
|
||||||
if (!m_project) return;
|
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;
|
if (folioIndex < 0 || folioIndex >= diagrams.count()) return;
|
||||||
diagrams.at(folioIndex)->clearSelection();
|
diagrams.at(folioIndex)->clearSelection();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,17 @@
|
|||||||
#include <QVariantList>
|
#include <QVariantList>
|
||||||
#include <QVariantMap>
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "../diagramcontext.h"
|
||||||
|
|
||||||
class QETProject;
|
class QETProject;
|
||||||
|
class Diagram;
|
||||||
class DiagramView;
|
class DiagramView;
|
||||||
class Element;
|
class Element;
|
||||||
class Terminal;
|
class Terminal;
|
||||||
class Conductor;
|
class Conductor;
|
||||||
|
class ConductorProperties;
|
||||||
class IndependentTextItem;
|
class IndependentTextItem;
|
||||||
class QetShapeItem;
|
class QetShapeItem;
|
||||||
class DiagramImageItem;
|
class DiagramImageItem;
|
||||||
@@ -435,6 +441,11 @@ class QetScriptApi : public QObject
|
|||||||
Q_INVOKABLE bool setConductorProperty(int folioIndex, const QString &elementUuid,
|
Q_INVOKABLE bool setConductorProperty(int folioIndex, const QString &elementUuid,
|
||||||
int terminalIndex, const QString &property,
|
int terminalIndex, const QString &property,
|
||||||
const QString &value);
|
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
|
// -- a conductor's own drawn path, not the whole potential's
|
||||||
// properties above -- one conductor only, addressed the same way --
|
// properties above -- one conductor only, addressed the same way --
|
||||||
@@ -642,6 +653,12 @@ class QetScriptApi : public QObject
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
bool runFlag(const QString &flag, const QStringList &args);
|
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;
|
Element *findElement(int folioIndex, const QString &elementUuid) const;
|
||||||
Terminal *findTerminal(int folioIndex, const QString &elementUuid, int terminalIndex,
|
Terminal *findTerminal(int folioIndex, const QString &elementUuid, int terminalIndex,
|
||||||
const QString &caller);
|
const QString &caller);
|
||||||
|
|||||||
@@ -88,6 +88,37 @@ void ConductorPropertiesDialog::PropertiesDialog(Conductor *conductor,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//A function separated terminal element (element information
|
||||||
|
//potential_isolating) keeps the two sides of the terminal apart:
|
||||||
|
//every property but one stays local to the side it was edited on.
|
||||||
|
//The voltage / protocol still crosses, because it describes the
|
||||||
|
//electrical node and not one side of it.
|
||||||
|
if (cpd.applyAll())
|
||||||
|
{
|
||||||
|
//Only an edit that really changed the voltage / protocol passes
|
||||||
|
//it on, so the comparison is the edit itself -- what the dialog
|
||||||
|
//was filled with, against what the user left in it -- and never
|
||||||
|
//the far wire's own value, which differs from it just as often
|
||||||
|
//as not (a 24 V side against a 0 V one, say) and would then be
|
||||||
|
//overwritten by every edit, voltage included.
|
||||||
|
const ConductorProperties shown = conductor->properties();
|
||||||
|
foreach (Conductor *separated_conductor, conductor->functionSeparatedConductors())
|
||||||
|
{
|
||||||
|
ConductorProperties separated_properties = separated_conductor->properties();
|
||||||
|
separated_properties.applyTensionProtocolChange(
|
||||||
|
shown,
|
||||||
|
cpd.properties());
|
||||||
|
if (separated_properties == separated_conductor->properties())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
QVariant separated_old_value, separated_new_value;
|
||||||
|
separated_old_value.setValue(separated_conductor->properties());
|
||||||
|
separated_new_value.setValue(separated_properties);
|
||||||
|
new QPropertyUndoCommand (separated_conductor, "properties",
|
||||||
|
separated_old_value, separated_new_value, undo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
conductor->diagram()->undoStack().push(undo);
|
conductor->diagram()->undoStack().push(undo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -346,7 +346,10 @@ void ConductorPropertiesEditorWidget::updateUi()
|
|||||||
When "apply to all" is ticked, every conductor on the same potential as
|
When "apply to all" is ticked, every conductor on the same potential as
|
||||||
an edited one is updated too, in the same undo step (one undo reverts
|
an edited one is updated too, in the same undo step (one undo reverts
|
||||||
them all), as the modal dialog does
|
them all), as the modal dialog does
|
||||||
(ConductorPropertiesDialog::PropertiesDialog).
|
(ConductorPropertiesDialog::PropertiesDialog). A terminal element with
|
||||||
|
its function separation enabled is not crossed by any of that -- its
|
||||||
|
other side keeps its own text, function, color... -- but the voltage /
|
||||||
|
protocol still crosses (@see Conductor::functionSeparatedConductors).
|
||||||
*/
|
*/
|
||||||
QUndoCommand *ConductorPropertiesEditorWidget::associatedUndo() const
|
QUndoCommand *ConductorPropertiesEditorWidget::associatedUndo() const
|
||||||
{
|
{
|
||||||
@@ -360,10 +363,12 @@ QUndoCommand *ConductorPropertiesEditorWidget::associatedUndo() const
|
|||||||
if (c)
|
if (c)
|
||||||
selected << c.data();
|
selected << c.data();
|
||||||
|
|
||||||
return ConductorMultiEdit::undo(
|
QSet<Conductor *> function_separated;
|
||||||
ConductorMultiEdit::targets(
|
const QList<Conductor *> targets = ConductorMultiEdit::targets(
|
||||||
selected, m_apply_all_cb && m_apply_all_cb->isChecked()),
|
selected, m_apply_all_cb && m_apply_all_cb->isChecked(),
|
||||||
m_shown, edited);
|
&function_separated);
|
||||||
|
|
||||||
|
return ConductorMultiEdit::undo(targets, m_shown, edited, function_separated);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -313,8 +313,9 @@ void ElementInfoWidget::buildInterface()
|
|||||||
|
|
||||||
ui->scroll_vlayout->addStretch();
|
ui->scroll_vlayout->addStretch();
|
||||||
|
|
||||||
// Existing potential isolating checkbox
|
// Existing function separation checkbox (stored as the
|
||||||
m_potential_isolating_cb = new QCheckBox(tr("Potential separation"), this);
|
// "potential_isolating" element information)
|
||||||
|
m_potential_isolating_cb = new QCheckBox(tr("Function separation"), this);
|
||||||
m_potential_isolating_cb->setStyleSheet(QStringLiteral("margin: 5px; font-weight: bold;"));
|
m_potential_isolating_cb->setStyleSheet(QStringLiteral("margin: 5px; font-weight: bold;"));
|
||||||
|
|
||||||
// English: Initialize and style the BOM exclusion checkbox
|
// English: Initialize and style the BOM exclusion checkbox
|
||||||
|
|||||||
@@ -13,6 +13,16 @@
|
|||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <algorithm>
|
#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
|
* @brief TerminalNumberingDialog::TerminalNumberingDialog
|
||||||
* Constructor
|
* Constructor
|
||||||
@@ -31,7 +41,7 @@ TerminalNumberingDialog::TerminalNumberingDialog(QWidget *parent, QETProject *pr
|
|||||||
// Collect all unique terminal strip prefixes from the project
|
// Collect all unique terminal strip prefixes from the project
|
||||||
if (project) {
|
if (project) {
|
||||||
QSet<QString> prefixes;
|
QSet<QString> prefixes;
|
||||||
foreach (Diagram *diagram, project->diagrams()) {
|
foreach (Diagram *diagram, terminalFolios(project)) {
|
||||||
foreach (QGraphicsItem *qgi, diagram->items()) {
|
foreach (QGraphicsItem *qgi, diagram->items()) {
|
||||||
if (Element *elmt = qgraphicsitem_cast<Element *>(qgi)) {
|
if (Element *elmt = qgraphicsitem_cast<Element *>(qgi)) {
|
||||||
if (elmt->elementData().m_type == ElementData::Terminal) {
|
if (elmt->elementData().m_type == ElementData::Terminal) {
|
||||||
@@ -153,7 +163,7 @@ QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const
|
|||||||
QList<TermInfo> termList;
|
QList<TermInfo> termList;
|
||||||
|
|
||||||
// 2. Collect all terminals from all folios in the project
|
// 2. Collect all terminals from all folios in the project
|
||||||
foreach (Diagram *diagram, project->diagrams()) {
|
foreach (Diagram *diagram, terminalFolios(project)) {
|
||||||
int fIndex = diagram->folioIndex();
|
int fIndex = diagram->folioIndex();
|
||||||
foreach (QGraphicsItem *qgi, diagram->items()) {
|
foreach (QGraphicsItem *qgi, diagram->items()) {
|
||||||
if (Element *elmt = qgraphicsitem_cast<Element *>(qgi)) {
|
if (Element *elmt = qgraphicsitem_cast<Element *>(qgi)) {
|
||||||
|
|||||||
@@ -119,3 +119,27 @@ void ChangeElementInformationCommand::updateProjectDB()
|
|||||||
elmt->diagram()->project()->dataBase()->elementInfoChanged(list_);
|
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 "../diagramcontext.h"
|
||||||
|
|
||||||
#include <QUndoCommand>
|
#include <QUndoCommand>
|
||||||
|
#include <QUuid>
|
||||||
|
#include <QPointer>
|
||||||
|
|
||||||
class Element;
|
class Element;
|
||||||
|
class QETProject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief The ChangeElementInformationCommand class
|
@brief The ChangeElementInformationCommand class
|
||||||
@@ -52,4 +55,26 @@ class ChangeElementInformationCommand : public QUndoCommand
|
|||||||
QMap<QPointer<Element>, QPair<DiagramContext, DiagramContext>> m_map;
|
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
|
#endif // CHANGEELEMENTINFORMATIONCOMMAND_H
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#include "wiringlistexport.h"
|
#include "wiringlistexport.h"
|
||||||
#include "qetproject.h"
|
#include "qetproject.h"
|
||||||
#include "diagram.h"
|
#include "diagram.h"
|
||||||
|
#include "dataBase/projectdatabase.h"
|
||||||
|
#include "ElementsCollection/xmlelementcollection.h"
|
||||||
|
#include "qetgraphicsitem/element.h"
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QTextStream>
|
#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"
|
// The folio number as the folio shows it ("3/12"). The saved "folio"
|
||||||
// attribute is its template ("%id/%total").
|
// attribute is its template ("%id/%total").
|
||||||
const int index = diagramElem.attribute("order").toInt();
|
const int index = diagramElem.attribute("order").toInt();
|
||||||
if (m_project && index >= 1 && index <= m_project->diagrams().size()) {
|
if (m_project && index >= 1 && index <= m_project->folios().size()) {
|
||||||
return m_project->diagrams().at(index - 1)->border_and_titleblock
|
return m_project->folios().at(index - 1)->border_and_titleblock
|
||||||
.titleblockInformation().value("folio").toString();
|
.titleblockInformation().value("folio").toString();
|
||||||
}
|
}
|
||||||
if (diagramElem.hasAttribute("folio")) return diagramElem.attribute("folio");
|
if (diagramElem.hasAttribute("folio")) return diagramElem.attribute("folio");
|
||||||
@@ -48,6 +51,89 @@ QDomElement WiringListExport::climbToDiagram(QDomNode node) const
|
|||||||
return QDomElement();
|
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> WiringListExport::collectElementsInfo(const QDomElement &root) const
|
||||||
{
|
{
|
||||||
QMap<QString, ElementInfo> infoMap;
|
QMap<QString, ElementInfo> infoMap;
|
||||||
@@ -191,7 +277,7 @@ QString WiringListExport::toCsvString() const
|
|||||||
{
|
{
|
||||||
if (!m_project) return QString();
|
if (!m_project) return QString();
|
||||||
|
|
||||||
QDomDocument doc = m_project->toXml();
|
QDomDocument doc = projectDocument();
|
||||||
if (doc.isNull()) return QString();
|
if (doc.isNull()) return QString();
|
||||||
|
|
||||||
QSet<QString> conductorDefinitionTypes;
|
QSet<QString> conductorDefinitionTypes;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
#include <QDomDocument>
|
||||||
|
|
||||||
class QETProject;
|
class QETProject;
|
||||||
class QWidget;
|
class QWidget;
|
||||||
@@ -60,6 +61,7 @@ private:
|
|||||||
QString normalizeUuid(const QString &u) const;
|
QString normalizeUuid(const QString &u) const;
|
||||||
QString findDiagramFolio(const QDomElement &diagramElem) const;
|
QString findDiagramFolio(const QDomElement &diagramElem) const;
|
||||||
QDomElement climbToDiagram(QDomNode node) const;
|
QDomElement climbToDiagram(QDomNode node) const;
|
||||||
|
QDomDocument projectDocument() const;
|
||||||
|
|
||||||
QMap<QString, ElementInfo> collectElementsInfo(const QDomElement &root) const;
|
QMap<QString, ElementInfo> collectElementsInfo(const QDomElement &root) const;
|
||||||
QList<ConductorData> collectConductors(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
|
# 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
|
# 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);
|
# (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
|
# 4. let it run again: the unfixed build aborts within a second, the fixed
|
||||||
# one keeps running until the test interrupts it.
|
# one keeps running until the test interrupts it.
|
||||||
#
|
#
|
||||||
@@ -104,14 +110,32 @@ if command -v nm >/dev/null; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
SANDBOX="$(mktemp -d /tmp/qet-modal-quit.XXXXXX)"
|
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
|
trap cleanup EXIT
|
||||||
|
|
||||||
# A unique binary path gives this run its own SingleApplication socket, so it
|
# 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
|
# can neither be captured by nor capture a QElectroTech already running. A
|
||||||
# symlink would not do: applicationFilePath() resolves it back to the real path.
|
# 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
|
# Keep the original file name. QElectroTech decides what to open from the
|
||||||
# extension, so copying a .elmt to "project.qet" would quietly turn an
|
# extension, so copying a .elmt to "project.qet" would quietly turn an
|
||||||
@@ -141,9 +165,16 @@ end
|
|||||||
break QDialog::exec
|
break QDialog::exec
|
||||||
run
|
run
|
||||||
delete
|
delete
|
||||||
|
if \$_isvoid(\$_exitcode)
|
||||||
printf "QET_TEST: dialog exec() entered\n"
|
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)
|
python qet_interrupt_later($SETTLE)
|
||||||
continue
|
continue
|
||||||
|
tbreak QEventLoop::processEvents thread 1
|
||||||
|
continue
|
||||||
|
printf "QET_TEST: at the dialog's event loop\n"
|
||||||
thread 1
|
thread 1
|
||||||
set \$app = ((void* (*)(void))'QETApp::instance()')()
|
set \$app = ((void* (*)(void))'QETApp::instance()')()
|
||||||
call ((void (*)(void*))'QETApp::quitQET()')(\$app)
|
call ((void (*)(void*))'QETApp::quitQET()')(\$app)
|
||||||
@@ -162,14 +193,29 @@ EOF
|
|||||||
# the full watchdog period no matter how fast gdb finished: ten seconds of
|
# 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.
|
# work reported as two minutes, a stone's throw from the CTest timeout.
|
||||||
GDB_TIMEOUT=120
|
GDB_TIMEOUT=120
|
||||||
if command -v timeout >/dev/null; then
|
# Each try under a binary path of its own: a new SingleApplication key. Tried
|
||||||
timeout --signal=KILL "$GDB_TIMEOUT" \
|
# 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" \
|
gdb -batch -x "$SANDBOX/scenario.gdb" --args "$TEST_BINARY" "$SANDBOX_PROJECT" \
|
||||||
> "$SANDBOX/gdb.log" 2>&1
|
> "$SANDBOX/gdb.log" 2>&1
|
||||||
else
|
fi
|
||||||
gdb -batch -x "$SANDBOX/scenario.gdb" --args "$TEST_BINARY" "$SANDBOX_PROJECT" \
|
if grep -q "QET_TEST: exited before any dialog" "$SANDBOX/gdb.log" \
|
||||||
> "$SANDBOX/gdb.log" 2>&1
|
&& grep -q "SingleApplication: Unable to" "$SANDBOX/gdb.log"; then
|
||||||
fi
|
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"
|
log="$SANDBOX/gdb.log"
|
||||||
if ! grep -q "QET_TEST: dialog exec() entered" "$log"; then
|
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>
|
--build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech>
|
||||||
--source ${CMAKE_CURRENT_SOURCE_DIR}/lazy_folios_probe.cpp
|
--source ${CMAKE_CURRENT_SOURCE_DIR}/lazy_folios_probe.cpp
|
||||||
--fixture ${CMAKE_CURRENT_SOURCE_DIR}/../../examples/industrial.qet)
|
--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")
|
ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||||
# Deleting a cable, undoing the delete twice and then editing anything
|
# Deleting a cable, undoing the delete twice and then editing anything
|
||||||
# has both cable commands tearing down the same cable. Runs under the
|
# has both cable commands tearing down the same cable. Runs under the
|
||||||
@@ -271,6 +271,19 @@ qet_add_app_test(tst_rotateselectionimage tst_rotateselectionimage.cpp)
|
|||||||
# it selected and records no undo step (#1414); the key goes to the view.
|
# it selected and records no undo step (#1414); the key goes to the view.
|
||||||
qet_add_app_test(tst_imagehandleescape tst_imagehandleescape.cpp)
|
qet_add_app_test(tst_imagehandleescape tst_imagehandleescape.cpp)
|
||||||
|
|
||||||
|
# A folio's pictures, decoded in parallel, each come back with its own
|
||||||
|
# pixels and position; an <image> with no picture does not stop the others.
|
||||||
|
qet_add_app_test(tst_folioimageload tst_folioimageload.cpp)
|
||||||
|
|
||||||
|
# SVG export with smaller pictures (--svg-small-pictures, the export dialog's
|
||||||
|
# option): a picture drawn at a tenth of its size is stored at four times
|
||||||
|
# that, a small one whole; without the option every picture is whole.
|
||||||
|
qet_add_app_test(tst_svgsmallpictures tst_svgsmallpictures.cpp)
|
||||||
|
add_dependencies(tst_svgsmallpictures qelectrotech)
|
||||||
|
target_link_libraries(tst_svgsmallpictures PRIVATE Qt::Svg)
|
||||||
|
target_compile_definitions(tst_svgsmallpictures PRIVATE
|
||||||
|
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||||
|
|
||||||
# TextResizeHandles: the corner handles that change the width of a text box,
|
# TextResizeHandles: the corner handles that change the width of a text box,
|
||||||
# on a plain QGraphicsTextItem with the "textWidth" property QET texts have.
|
# on a plain QGraphicsTextItem with the "textWidth" property QET texts have.
|
||||||
add_executable(
|
add_executable(
|
||||||
@@ -735,6 +748,13 @@ target_link_libraries(tst_pdfpagesize PRIVATE Qt::Test Qt::Gui)
|
|||||||
target_compile_definitions(tst_pdfpagesize PRIVATE
|
target_compile_definitions(tst_pdfpagesize PRIVATE
|
||||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||||
|
|
||||||
|
# The print window's PDF export turns each page to its folio when "Fit in
|
||||||
|
# page" is on (#1480), and keeps the chosen orientation when it is off. The
|
||||||
|
# links on a turned page are checked on examples/industrial.qet.
|
||||||
|
qet_add_app_test(tst_printpdforientation tst_printpdforientation.cpp)
|
||||||
|
target_compile_definitions(tst_printpdforientation PRIVATE
|
||||||
|
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||||
|
|
||||||
# --export-pdf writes the cross-reference links in the same order in every
|
# --export-pdf writes the cross-reference links in the same order in every
|
||||||
# run, rather than in Element pointer order. Exports examples/industrial.qet
|
# run, rather than in Element pointer order. Exports examples/industrial.qet
|
||||||
# several times, in separate processes.
|
# several times, in separate processes.
|
||||||
@@ -1060,10 +1080,13 @@ endif()
|
|||||||
|
|
||||||
add_executable(
|
add_executable(
|
||||||
tst_resaveunchanged
|
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_test(NAME tst_resaveunchanged COMMAND tst_resaveunchanged)
|
||||||
add_dependencies(tst_resaveunchanged qelectrotech)
|
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
|
target_compile_definitions(tst_resaveunchanged PRIVATE
|
||||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||||
@@ -1253,4 +1276,6 @@ add_test(NAME tst_qetcontainer COMMAND tst_qetcontainer)
|
|||||||
target_include_directories(tst_qetcontainer PRIVATE ${QET_DIR}/sources)
|
target_include_directories(tst_qetcontainer PRIVATE ${QET_DIR}/sources)
|
||||||
target_link_libraries(tst_qetcontainer PRIVATE Qt::Test Qt::Xml SQLite3::SQLite3)
|
target_link_libraries(tst_qetcontainer PRIVATE Qt::Test Qt::Xml SQLite3::SQLite3)
|
||||||
target_compile_definitions(tst_qetcontainer PRIVATE
|
target_compile_definitions(tst_qetcontainer PRIVATE
|
||||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\""
|
||||||
|
"QET_FIXTURES_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/fixtures\""
|
||||||
|
"QET_FORMAT_SPEC=\"${QET_DIR}/sources/container/QETZ-FORMAT.md\"")
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,41 @@
|
|||||||
|
<project title="A large picture shown small" uuid="{3f6a2c91-8d47-4b5e-a0c3-91e2d7b4f600}" version="0.200.1">
|
||||||
|
<properties>
|
||||||
|
<property name="saveddate" show="1">10/2/26</property>
|
||||||
|
<property name="saveddate-eu" show="1">02-10-2026</property>
|
||||||
|
<property name="saveddate-us" show="1">2026-10-02</property>
|
||||||
|
<property name="savedfilename" show="1">svg_small_pictures</property>
|
||||||
|
<property name="savedtime" show="1">18:39</property>
|
||||||
|
</properties>
|
||||||
|
<usage enabled="true" time_spent="0"/>
|
||||||
|
<newdiagrams>
|
||||||
|
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
|
||||||
|
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
|
||||||
|
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||||
|
<report label="%f-%l%c"/>
|
||||||
|
<xrefs>
|
||||||
|
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
|
||||||
|
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
|
||||||
|
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
|
||||||
|
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
|
||||||
|
</xrefs>
|
||||||
|
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
|
||||||
|
<folio_autonums/>
|
||||||
|
<element_autonums current_autonum="" freeze_new_elements="false"/>
|
||||||
|
<guides/>
|
||||||
|
</newdiagrams>
|
||||||
|
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{3f6a2c91-8d47-4b5e-a0c3-91e2d7b4f610}" version="0.200.1-dev">
|
||||||
|
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||||
|
<inputs/>
|
||||||
|
<images>
|
||||||
|
<image z="0" rotation="0" y="100" x="100" size="0.1" is_movable="1">iVBORw0KGgoAAAANSUhEUgAAAyAAAAJYCAIAAAAVFBUnAAAK8ElEQVR42u3ZMQ7CQBAEwTPi4TydiMCxOxihqogI6RyNeq9zPufnevbjP/7Bc3wQz/FBPMcH8Rwf5OE/vA4AACkDCwAg9r7lMAAAHlOwAABiChYAQEzBAgAwsAAAtjkRAgDEFCwAgJiCBQAQU7AAAGIKFgBATMECADCwAAC2ORECAMQULACAmIIFABBTsAAAYgoWAEBMwQIAMLAAALY5EQIAxBQsAICYggUAEFOwAABiChYAQEzBAgAwsAAAtjkRAgDEFCwAgJiCBQAQU7AAAGIKFgBATMECADCwAAC2ORECAMQULACAmIIFABBTsAAAYgoWAEBMwQIAMLAAALY5EQIAxBQsAICYggUAEFOwAAAMLACAbU6EAAAxBQsAIKZgAQDEFCwAgJiCBQAQU7AAAAwsAIBtToQAADEFCwAgpmABAMQULACAmIIFABBTsAAADCwAgG1OhAAAMQULACCmYAEAxBQsAICYggUAEFOwAAAMLACAbU6EAAAxBQsAIKZgAQDEFCwAgJiCBQAQU7AAAAwsAIBtToQAADEFCwAgpmABAMQULACAmIIFABBTsAAADCwAgG1OhAAAMQULACCmYAEAxBQsAAADCwBgmxMhAEBMwQIAiClYAAAxBQsAIKZgAQDEFCwAAAMLAGCbEyEAQEzBAgCIKVgAADEFCwAgpmABAMQULAAAAwsAYJsTIQBATMECAIgpWAAAMQULACCmYAEAxBQsAAADCwBgmxMhAEBMwQIAiClYAAAxBQsAIKZgAQDEFCwAAAMLAGCbEyEAQEzBAgCIKVgAADEFCwAgpmABAMQULAAAAwsAYJsTIQBATMECAIgpWAAAMQULAMDAAgDY5kQIABBTsAAAYgoWAEBMwQIAiClYAAAxBQsAwMACANjmRAgAEFOwAABiChYAQEzBAgCIKVgAADEFCwDAwAIA2OZECAAQU7AAAGIKFgBATMECAIgpWAAAMQULAMDAAgDY5kQIABBTsAAAYgoWAEBMwQIAiClYAAAxBQsAwMACANjmRAgAEFOwAABiChYAQEzBAgCIKVgAADEFCwDAwAIA2OZECAAQU7AAAGIKFgBATMECADCwAAC2ORECAMQULACAmIIFABBTsAAAYgoWAEBMwQIAMLAAALY5EQIAxBQsAICYggUAEFOwAABiChYAQEzBAgAwsAAAtjkRAgDEFCwAgJiCBQAQU7AAAGIKFgBATMECADCwAAC2ORECAMQULACAmIIFABBTsAAAYgoWAEBMwQIAMLAAALY5EQIAxBQsAICYggUAEFOwAABiChYAQEzBAgAwsAAAtjkRAgDEFCwAgJiCBQAQU7AAAAwsAIBtToQAADEFCwAgpmABAMQULACAmIIFABBTsAAADCwAgG1OhAAAMQULACCmYAEAxBQsAICYggUAEFOwAAAMLACAbU6EAAAxBQsAIKZgAQDEFCwAgJiCBQAQU7AAAAwsAIBtToQAADEFCwAgpmABAMQULACAmIIFABBTsAAADCwAgG1OhAAAMQULACCmYAEAxBQsAICYggUAEFOwAAAMLACAbU6EAAAxBQsAIKZgAQDEFCwAAAMLAGCbEyEAQEzBAgCIKVgAADEFCwAgpmABAMQULAAAAwsAYJsTIQBATMECAIgpWAAAMQULACCmYAEAxBQsAAADCwBgmxMhAEBMwQIAiClYAAAxBQsAIKZgAQDEFCwAAAMLAGCbEyEAQEzBAgCIKVgAADEFCwAgpmABAMQULAAAAwsAYJsTIQBATMECAIgpWAAAMQULACCmYAEAxBQsAAADCwBgmxMhAEBMwQIAiClYAAAxBQsAwMACANjmRAgAEFOwAABiChYAQEzBAgCIKVgAADEFCwDAwAIA2OZECAAQU7AAAGIKFgBATMECAIgpWAAAMQULAMDAAgDY5kQIABBTsAAAYgoWAEBMwQIAiClYAAAxBQsAwMACANjmRAgAEFOwAABiChYAQEzBAgCIKVgAADEFCwDAwAIA2OZECAAQU7AAAGIKFgBATMECAIgpWAAAMQULAMDAAgDY5kQIABBTsAAAYgoWAEBMwQIAMLAAALY5EQIAxBQsAICYggUAEFOwAABiChYAQEzBAgAwsAAAtjkRAgDEFCwAgJiCBQAQU7AAAGIKFgBATMECADCwAAC2ORECAMQULACAmIIFABBTsAAAYgoWAEBMwQIAMLAAALY5EQIAxBQsAICYggUAEFOwAABiChYAQEzBAgAwsAAAtjkRAgDEFCwAgJiCBQAQU7AAAGIKFgBATMECADCwAAC2ORECAMQULACAmIIFABBTsAAADCwAgG1OhAAAMQULACCmYAEAxBQsAICYggUAEFOwAAAMLACAbU6EAAAxBQsAIKZgAQDEFCwAgJiCBQAQU7AAAAwsAIBtToQAADEFCwAgpmABAMQULACAmIIFABBTsAAADCwAgG1OhAAAMQULACCmYAEAxBQsAICYggUAEFOwAAAMLACAbU6EAAAxBQsAIKZgAQDEFCwAgJiCBQAQU7AAAAwsAIBtToQAADEFCwAgpmABAMQULAAAAwsAYJsTIQBATMECAIgpWAAAMQULACCmYAEAxBQsAAADCwBgmxMhAEBMwQIAiClYAAAxBQsAIKZgAQDEFCwAAAMLAGCbEyEAQEzBAgCIKVgAADEFCwAgpmABAMQULAAAAwsAYJsTIQBATMECAIgpWAAAMQULACCmYAEAxBQsAAADCwBgmxMhAEBMwQIAiClYAAAxBQsAIKZgAQDEFCwAAAMLAGCbEyEAQEzBAgCIKVgAADEFCwDAwAIA2OZECAAQU7AAAGIKFgBATMECAIgpWAAAMQULAMDAAgDY5kQIABBTsAAAYgoWAEBMwQIAiClYAAAxBQsAwMACANjmRAgAEFOwAABiChYAQEzBAgCIKVgAADEFCwDAwAIA2OZECAAQU7AAAGIKFgBATMECAIgpWAAAMQULAMDAAgDY5kQIABBTsAAAYgoWAEBMwQIAiClYAAAxBQsAwMACANjmRAgAEFOwAABiChYAQEzBAgAwsAAAtjkRAgDEFCwAgJiCBQAQU7AAAGIKFgBATMECADCwAAC2ORECAMQULACAmIIFABBTsAAAYgoWAEBMwQIAMLAAALY5EQIAxBQsAICYggUAEFOwAABiChYAQEzBAgAwsAAAtjkRAgDEFCwAgJiCBQAQU7AAAGIKFgBATMECADCwAAC2ORECAMQULACAmIIFABBTsAAAYgoWAEBMwQIAMLAAALY5EQIAxBQsAICYggUAEFOwAAAMLACAbU6EAAAxBQsAIKZgAQDEFCwAgJiCBQAQU7AAAAwsAIBtToQAADEFCwAgpmABAMQULACAmIIFABBTsAAADCwAgG1OhAAAMQULACCmYAEAxBQsAICYggUAEFOwAAAMLACAbU6EAAAxBQsAIKZgAQDEFCwAgJiCBQAQU7AAAAwsAIBtToQAADEFCwAgpmABAMQULACAmIIFABBTsAAADCwAgG1OhAAAMQULACCmYAEAxBQsAAADCwBgmxMhAEBMwQIAiClYAAAxBQsAIKZgAQDEFCwAAAMLAGCbEyEAQEzBAgCIKVgAADEFCwAgpmABAMQULAAAAwsAYJsTIQBATMECAIgpWAAAMQULACCmYAEAxBQsAAADCwBgmxMhAEBMwQIAiClYAAAxBQsAIKZgAQDEFCwAAAMLAGCbEyEAQEzBAgCIKVgAADEFCwAg9gUwEwkox8wwtQAAAABJRU5ErkJggg==</image>
|
||||||
|
<image z="0" rotation="0" y="300" x="100" size="1" is_movable="1">iVBORw0KGgoAAAANSUhEUgAAACgAAAAeCAIAAADRv8uKAAAAP0lEQVR42u3PMQoAIAwEwRVC1F/7dFuxFoKwqS7XHNNgJSR0OMP1Pu+DQckFs2pYsWLFihUrVqxYsWLFir8Rb7mhAuQBqxaIAAAAAElFTkSuQmCC</image>
|
||||||
|
</images>
|
||||||
|
</diagram>
|
||||||
|
<collection>
|
||||||
|
<category name="import">
|
||||||
|
<names>
|
||||||
|
<name lang="en">Imported elements</name>
|
||||||
|
</names>
|
||||||
|
</category>
|
||||||
|
</collection>
|
||||||
|
</project>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
// Lazy folios (QET_LAZY_FOLIOS, LAZY-FOLIO-PLAN.md stages 5.2 and 5.3),
|
// 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
|
// 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 without building its folios draws and links as in the project
|
||||||
// opened as usual, building only it and the folios its links and tables
|
// opened as usual, building only it and the folios its links and tables
|
||||||
@@ -23,22 +23,99 @@
|
|||||||
#include "../../sources/qetgraphicsitem/element.h"
|
#include "../../sources/qetgraphicsitem/element.h"
|
||||||
#include "../../sources/autoNum/elementautonumschemecommand.h"
|
#include "../../sources/autoNum/elementautonumschemecommand.h"
|
||||||
#include "../../sources/elementprovider.h"
|
#include "../../sources/elementprovider.h"
|
||||||
|
#include "../../sources/container/qetzip.h"
|
||||||
|
#include <QSqlDatabase>
|
||||||
|
#include <QMap>
|
||||||
#include "../../sources/TerminalStrip/terminalstrip.h"
|
#include "../../sources/TerminalStrip/terminalstrip.h"
|
||||||
#include "../../sources/ui/elementpropertieswidget.h"
|
#include "../../sources/ui/elementpropertieswidget.h"
|
||||||
#include "../../sources/conductorautonumerotation.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/diagramcontent.h"
|
||||||
#include "../../sources/diagramview.h"
|
#include "../../sources/diagramview.h"
|
||||||
#include "../../sources/diagramevent/diagrameventaddpaste.h"
|
#include "../../sources/diagramevent/diagrameventaddpaste.h"
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
#include "../../sources/qetgraphicsitem/conductor.h"
|
#include "../../sources/qetgraphicsitem/conductor.h"
|
||||||
|
#include "../../sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
|
||||||
#include "../../sources/qetgraphicsitem/terminal.h"
|
#include "../../sources/qetgraphicsitem/terminal.h"
|
||||||
#include "../../sources/undocommand/changeelementinformationcommand.h"
|
#include "../../sources/undocommand/changeelementinformationcommand.h"
|
||||||
#include "../../sources/undocommand/changetitleblockcommand.h"
|
#include "../../sources/undocommand/changetitleblockcommand.h"
|
||||||
#include "../../sources/undocommand/deleteqgraphicsitemcommand.h"
|
#include "../../sources/undocommand/deleteqgraphicsitemcommand.h"
|
||||||
#include "../../sources/factory/elementfactory.h"
|
#include "../../sources/factory/elementfactory.h"
|
||||||
#include "../../sources/undocommand/addgraphicsobjectcommand.h"
|
#include "../../sources/undocommand/addgraphicsobjectcommand.h"
|
||||||
|
#include "../../sources/dataBase/projectdatabase.h"
|
||||||
|
#include <QDomDocument>
|
||||||
|
|
||||||
static void check(bool value, const char *message) { if (!value) throw std::runtime_error(message); }
|
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
|
//Every symbol's numbering, in a stable order
|
||||||
static QStringList numbering(const QETProject &project)
|
static QStringList numbering(const QETProject &project)
|
||||||
@@ -174,6 +251,55 @@ static QString saved(QETProject &project)
|
|||||||
return QETProject::spliceKeptFolios(document.toString(4), kept);
|
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
|
//The rows of the project database's tables, in their order, read as
|
||||||
//every caller reads them (newQuery(): brought up to date first)
|
//every caller reads them (newQuery(): brought up to date first)
|
||||||
static QStringList tables(QETProject &project)
|
static QStringList tables(QETProject &project)
|
||||||
@@ -238,7 +364,7 @@ int main(int argc, char **argv)
|
|||||||
qputenv("QET_LAZY_SAVE_KEPT", "1");
|
qputenv("QET_LAZY_SAVE_KEPT", "1");
|
||||||
try {
|
try {
|
||||||
const QString output = QString::fromLocal8Bit(argv[3]);
|
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,
|
//A file saved by this version: its folios carry their uuids,
|
||||||
//which opening without building them needs
|
//which opening without building them needs
|
||||||
const QString path = output + "/lazy_folios.qet";
|
const QString path = output + "/lazy_folios.qet";
|
||||||
@@ -265,9 +391,9 @@ int main(int argc, char **argv)
|
|||||||
QETProject eager(path);
|
QETProject eager(path);
|
||||||
check(eager.state() == QETProject::Ok, "project opens");
|
check(eager.state() == QETProject::Ok, "project opens");
|
||||||
{
|
{
|
||||||
qputenv("QET_LAZY_FOLIOS", "1");
|
qunsetenv("QET_EAGER_FOLIOS");
|
||||||
QETProject lazy_strips(path);
|
QETProject lazy_strips(path);
|
||||||
qunsetenv("QET_LAZY_FOLIOS");
|
qputenv("QET_EAGER_FOLIOS", "1");
|
||||||
auto strip_of = [](const QETProject &project) {
|
auto strip_of = [](const QETProject &project) {
|
||||||
for (TerminalStrip *strip : project.terminalStrip())
|
for (TerminalStrip *strip : project.terminalStrip())
|
||||||
if (strip->name() == QStringLiteral("X99")) return strip->physicalTerminalCount();
|
if (strip->name() == QStringLiteral("X99")) return strip->physicalTerminalCount();
|
||||||
@@ -288,7 +414,7 @@ int main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
check(linked > 0, "the project has linked symbols");
|
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) {
|
auto same = [&](QETProject &lazy, int i, const char *what) {
|
||||||
Diagram *folio = lazy.folios().at(i);
|
Diagram *folio = lazy.folios().at(i);
|
||||||
lazy.buildFolioToShow(folio);
|
lazy.buildFolioToShow(folio);
|
||||||
@@ -321,6 +447,24 @@ int main(int argc, char **argv)
|
|||||||
for (const QString &title : titles)
|
for (const QString &title : titles)
|
||||||
check(gaps(numbered, title) == gaps(eager, title), "a scheme's gaps as when built");
|
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");
|
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
|
//A symbol placed on a folio, numbered by a scheme others follow
|
||||||
QString location, title;
|
QString location, title;
|
||||||
int folio = -1;
|
int folio = -1;
|
||||||
@@ -398,9 +542,9 @@ int main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
QETProject edited(path), usual(path);
|
QETProject edited(path), usual(path);
|
||||||
qunsetenv("QET_LAZY_FOLIOS");
|
qputenv("QET_EAGER_FOLIOS", "1");
|
||||||
QETProject usual_edited(path);
|
QETProject usual_edited(path);
|
||||||
qputenv("QET_LAZY_FOLIOS", "1");
|
qunsetenv("QET_EAGER_FOLIOS");
|
||||||
edit(edited, busiest);
|
edit(edited, busiest);
|
||||||
std::printf("edits on folio %d (%d links to other folios): %d folios not built\n",
|
std::printf("edits on folio %d (%d links to other folios): %d folios not built\n",
|
||||||
busiest + 1, most, edited.unloadedFolioCount());
|
busiest + 1, most, edited.unloadedFolioCount());
|
||||||
@@ -480,9 +624,9 @@ int main(int argc, char **argv)
|
|||||||
//After everyday edits on a folio: as when built, others not built
|
//After everyday edits on a folio: as when built, others not built
|
||||||
{
|
{
|
||||||
QETProject edited(path), usual(path);
|
QETProject edited(path), usual(path);
|
||||||
qunsetenv("QET_LAZY_FOLIOS");
|
qputenv("QET_EAGER_FOLIOS", "1");
|
||||||
QETProject usual_edited(path);
|
QETProject usual_edited(path);
|
||||||
qputenv("QET_LAZY_FOLIOS", "1");
|
qunsetenv("QET_EAGER_FOLIOS");
|
||||||
edit(edited, busiest);
|
edit(edited, busiest);
|
||||||
edit(usual_edited, busiest);
|
edit(usual_edited, busiest);
|
||||||
QFile file(path); file.open(QIODevice::ReadOnly);
|
QFile file(path); file.open(QIODevice::ReadOnly);
|
||||||
@@ -512,9 +656,9 @@ int main(int argc, char **argv)
|
|||||||
//links reach are built to be saved, the others not
|
//links reach are built to be saved, the others not
|
||||||
{
|
{
|
||||||
QETProject renamed(path);
|
QETProject renamed(path);
|
||||||
qunsetenv("QET_LAZY_FOLIOS");
|
qputenv("QET_EAGER_FOLIOS", "1");
|
||||||
QETProject usual_renamed(path);
|
QETProject usual_renamed(path);
|
||||||
qputenv("QET_LAZY_FOLIOS", "1");
|
qunsetenv("QET_EAGER_FOLIOS");
|
||||||
for (QETProject *project : {&renamed, &usual_renamed}) {
|
for (QETProject *project : {&renamed, &usual_renamed}) {
|
||||||
Diagram *folio = project->folios().at(busiest);
|
Diagram *folio = project->folios().at(busiest);
|
||||||
TitleBlockProperties block = folio->border_and_titleblock.exportTitleBlock();
|
TitleBlockProperties block = folio->border_and_titleblock.exportTitleBlock();
|
||||||
@@ -529,9 +673,9 @@ int main(int argc, char **argv)
|
|||||||
//folio is built to be saved
|
//folio is built to be saved
|
||||||
{
|
{
|
||||||
QETProject changed(path);
|
QETProject changed(path);
|
||||||
qunsetenv("QET_LAZY_FOLIOS");
|
qputenv("QET_EAGER_FOLIOS", "1");
|
||||||
QETProject usual_changed(path);
|
QETProject usual_changed(path);
|
||||||
qputenv("QET_LAZY_FOLIOS", "1");
|
qunsetenv("QET_EAGER_FOLIOS");
|
||||||
for (QETProject *project : {&changed, &usual_changed}) {
|
for (QETProject *project : {&changed, &usual_changed}) {
|
||||||
DiagramContext properties = project->projectProperties();
|
DiagramContext properties = project->projectProperties();
|
||||||
properties.addValue(QStringLiteral("lazy_test"), QStringLiteral("1"));
|
properties.addValue(QStringLiteral("lazy_test"), QStringLiteral("1"));
|
||||||
@@ -567,6 +711,446 @@ int main(int argc, char **argv)
|
|||||||
check(potential(wire) == potential(usual_wire), "a potential followed as when built");
|
check(potential(wire) == potential(usual_wire), "a potential followed as when built");
|
||||||
std::printf("potential through reports: %d wires, %d folios not built\n",
|
std::printf("potential through reports: %d wires, %d folios not built\n",
|
||||||
int(potential(wire).size()), followed.unloadedFolioCount());
|
int(potential(wire).size()), followed.unloadedFolioCount());
|
||||||
|
}
|
||||||
|
//Two symbols sharing a uuid: the database is filled from the
|
||||||
|
//folios, which builds every one of them, so the project opens
|
||||||
|
//with every folio built rather than building them all later
|
||||||
|
{
|
||||||
|
QFile file(path);
|
||||||
|
check(file.open(QIODevice::ReadOnly), "fixture read");
|
||||||
|
QDomDocument document;
|
||||||
|
check(bool(document.setContent(&file)), "fixture parsed");
|
||||||
|
file.close();
|
||||||
|
check(projectDataBase::documentRefusal(document).isEmpty(), "the fixture fills the database itself");
|
||||||
|
QDomNodeList elements = document.elementsByTagName(QStringLiteral("element"));
|
||||||
|
QList<QDomElement> placed;
|
||||||
|
for (int i = 0; i < elements.size(); ++i) {
|
||||||
|
QDomElement e = elements.at(i).toElement();
|
||||||
|
if (Element::valideXml(e)) placed << e;
|
||||||
|
}
|
||||||
|
check(placed.size() > 1, "two placed symbols");
|
||||||
|
placed.last().setAttribute(QStringLiteral("uuid"), placed.first().attribute(QStringLiteral("uuid")));
|
||||||
|
check(!projectDataBase::documentRefusal(document).isEmpty(), "a shared uuid is seen before the folios are read");
|
||||||
|
const QString shared = output + "/lazy_folios_shared_uuid.qet";
|
||||||
|
QFile out(shared);
|
||||||
|
check(out.open(QIODevice::WriteOnly), "copy written");
|
||||||
|
out.write(document.toByteArray(4));
|
||||||
|
out.close();
|
||||||
|
qunsetenv("QET_EAGER_FOLIOS");
|
||||||
|
qputenv("QET_LAZY_FOLIOS_TRACE", "1");
|
||||||
|
static int built_late; built_late = 0;
|
||||||
|
const QtMessageHandler previous = qInstallMessageHandler(
|
||||||
|
[](QtMsgType, const QMessageLogContext &, const QString &message) {
|
||||||
|
if (message.contains(QStringLiteral("built for a caller of diagrams()"))) ++built_late;
|
||||||
|
});
|
||||||
|
QETProject opened(shared);
|
||||||
|
qInstallMessageHandler(previous);
|
||||||
|
qunsetenv("QET_LAZY_FOLIOS_TRACE");
|
||||||
|
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()));
|
std::printf("PASS: %d folios\n", int(folios.size()));
|
||||||
} catch (const std::exception &error) {
|
} 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 = build / 'lazy-folios'
|
||||||
output.mkdir(exist_ok=True)
|
output.mkdir(exist_ok=True)
|
||||||
report = output / 'results.txt'
|
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():
|
if report.exists():
|
||||||
print(report.read_text(encoding='utf-8', errors='replace'))
|
print(report.read_text(encoding='utf-8', errors='replace'))
|
||||||
sys.exit(result.returncode)
|
sys.exit(result.returncode)
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ class FakeWire : public QObject
|
|||||||
ConductorProperties properties() const { return m_properties; }
|
ConductorProperties properties() const { return m_properties; }
|
||||||
void setProperties(const ConductorProperties &p) { m_properties = p; }
|
void setProperties(const ConductorProperties &p) { m_properties = p; }
|
||||||
QSet<FakeWire *> relatedPotentialConductors() const { return m_potential; }
|
QSet<FakeWire *> relatedPotentialConductors() const { return m_potential; }
|
||||||
|
// The wires of the other side of a function separated terminal
|
||||||
|
// element: same electrical node, but only the voltage /
|
||||||
|
// protocol crosses.
|
||||||
|
QSet<FakeWire *> functionSeparatedConductors() const { return m_separated; }
|
||||||
QPointF pos() const { return m_pos; }
|
QPointF pos() const { return m_pos; }
|
||||||
|
|
||||||
// Joins wires into one potential; like the real one, a wire's
|
// Joins wires into one potential; like the real one, a wire's
|
||||||
@@ -41,10 +45,24 @@ class FakeWire : public QObject
|
|||||||
w->m_potential.insert(o);
|
w->m_potential.insert(o);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Marks two groups as separated by a function separated
|
||||||
|
// terminal element, without joining their potentials.
|
||||||
|
static void separate(const QList<FakeWire *> &one,
|
||||||
|
const QList<FakeWire *> &other)
|
||||||
|
{
|
||||||
|
for (FakeWire *w : one)
|
||||||
|
for (FakeWire *o : other)
|
||||||
|
{
|
||||||
|
w->m_separated.insert(o);
|
||||||
|
o->m_separated.insert(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ConductorProperties m_properties;
|
ConductorProperties m_properties;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QSet<FakeWire *> m_potential;
|
QSet<FakeWire *> m_potential;
|
||||||
|
QSet<FakeWire *> m_separated;
|
||||||
QPointF m_pos;
|
QPointF m_pos;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,6 +120,143 @@ class tst_conductormultiedit : public QObject
|
|||||||
(QSet<FakeWire *> {&a, &b, &c, &d}));
|
(QSet<FakeWire *> {&a, &b, &c, &d}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A function separated terminal element: the other side of the
|
||||||
|
// terminal is reached too, but only the voltage / protocol
|
||||||
|
// crosses it -- not the function, not the number.
|
||||||
|
void functionSeparatedGetsVoltageProtocolOnly()
|
||||||
|
{
|
||||||
|
FakeWire a("1", "fnA", "", {}), b("2", "fnB", "", {}),
|
||||||
|
c("3", "fnC", "", {});
|
||||||
|
FakeWire::join({&a, &b});
|
||||||
|
FakeWire::separate({&a}, {&c});
|
||||||
|
|
||||||
|
QSet<FakeWire *> separated;
|
||||||
|
const Wires t = ConductorMultiEdit::targets(Wires {&a}, true,
|
||||||
|
&separated);
|
||||||
|
QCOMPARE(QSet<FakeWire *>(t.begin(), t.end()),
|
||||||
|
(QSet<FakeWire *> {&a, &b, &c}));
|
||||||
|
QCOMPARE(separated, (QSet<FakeWire *> {&c}));
|
||||||
|
// A caller that does not know about the rule gets the
|
||||||
|
// separated side left out, as before
|
||||||
|
QCOMPARE(ConductorMultiEdit::targets(Wires {&a}, true),
|
||||||
|
(Wires {&a, &b}));
|
||||||
|
|
||||||
|
const ConductorProperties shown = a.properties();
|
||||||
|
ConductorProperties after = edited(shown,
|
||||||
|
&ConductorProperties::m_function, "PWR");
|
||||||
|
after.m_tension_protocol = "24V";
|
||||||
|
|
||||||
|
QUndoStack stack;
|
||||||
|
stack.push(ConductorMultiEdit::undo(t, shown, after, separated));
|
||||||
|
QCOMPARE(stack.count(), 1);
|
||||||
|
|
||||||
|
for (FakeWire *w : {&a, &b})
|
||||||
|
{
|
||||||
|
QCOMPARE(w->m_properties.m_function, QStringLiteral("PWR"));
|
||||||
|
QCOMPARE(w->m_properties.m_tension_protocol,
|
||||||
|
QStringLiteral("24V"));
|
||||||
|
}
|
||||||
|
QCOMPARE(c.m_properties.m_function, QStringLiteral("fnC"));
|
||||||
|
QCOMPARE(c.m_properties.text, QStringLiteral("3"));
|
||||||
|
QCOMPARE(c.m_properties.m_tension_protocol, QStringLiteral("24V"));
|
||||||
|
|
||||||
|
stack.undo();
|
||||||
|
QVERIFY(c.properties().m_tension_protocol.isEmpty());
|
||||||
|
QCOMPARE(c.m_properties.m_function, QStringLiteral("fnC"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The explicitly picked side of a separation: selecting one
|
||||||
|
// wire on each side and editing them edits them both, in
|
||||||
|
// full -- however the selection happens to be walked (#review)
|
||||||
|
void selectedWireIsNeverSeparated()
|
||||||
|
{
|
||||||
|
FakeWire a("1", "fnA", "", {}), c("3", "fnC", "", {});
|
||||||
|
FakeWire::separate({&a}, {&c});
|
||||||
|
|
||||||
|
const ConductorProperties original_a = a.properties();
|
||||||
|
const ConductorProperties original_c = c.properties();
|
||||||
|
const ConductorProperties after = edited(original_a,
|
||||||
|
&ConductorProperties::m_function, "PWR");
|
||||||
|
|
||||||
|
for (const Wires &selection : {Wires {&a, &c}, Wires {&c, &a}})
|
||||||
|
{
|
||||||
|
//Each order starts from the same, unedited state
|
||||||
|
a.setProperties(original_a);
|
||||||
|
c.setProperties(original_c);
|
||||||
|
|
||||||
|
QSet<FakeWire *> separated;
|
||||||
|
const Wires t = ConductorMultiEdit::targets(selection, true,
|
||||||
|
&separated);
|
||||||
|
//c is picked, so it is not "the far side" of anything
|
||||||
|
QCOMPARE(separated, (QSet<FakeWire *> {}));
|
||||||
|
|
||||||
|
QUndoStack stack;
|
||||||
|
QUndoCommand *undo = ConductorMultiEdit::undo(t, original_a,
|
||||||
|
after, separated);
|
||||||
|
QVERIFY(undo);
|
||||||
|
stack.push(undo);
|
||||||
|
QCOMPARE(a.m_properties.m_function, QStringLiteral("PWR"));
|
||||||
|
QCOMPARE(c.m_properties.m_function, QStringLiteral("PWR"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A wire that belongs to the potential as well -- on any path,
|
||||||
|
// however far that path runs -- takes the full edit, even when a
|
||||||
|
// separation also reaches it. The fake's potential is one hop
|
||||||
|
// deep, unlike the real one, so the shared potential is stated
|
||||||
|
// in full here.
|
||||||
|
void wireReachableBothWaysIsNotSeparated()
|
||||||
|
{
|
||||||
|
FakeWire a("1", "fnA", "", {}), b("2", "fnB", "", {}),
|
||||||
|
c("3", "fnC", "", {});
|
||||||
|
FakeWire::join({&a, &b, &c});
|
||||||
|
FakeWire::separate({&a}, {&c});
|
||||||
|
|
||||||
|
QSet<FakeWire *> separated;
|
||||||
|
const Wires t = ConductorMultiEdit::targets(Wires {&a}, true,
|
||||||
|
&separated);
|
||||||
|
QCOMPARE(QSet<FakeWire *>(t.begin(), t.end()),
|
||||||
|
(QSet<FakeWire *> {&a, &b, &c}));
|
||||||
|
QCOMPARE(separated, (QSet<FakeWire *> {}));
|
||||||
|
|
||||||
|
const ConductorProperties shown = a.properties();
|
||||||
|
QUndoStack stack;
|
||||||
|
stack.push(ConductorMultiEdit::undo(t, shown, edited(shown,
|
||||||
|
&ConductorProperties::m_function, "PWR"), separated));
|
||||||
|
QCOMPARE(c.m_properties.m_function, QStringLiteral("PWR"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The far side only takes the voltage when the edit changed it:
|
||||||
|
// editing the function of a 24 V wire must leave the 0 V one
|
||||||
|
// at 0 V, not drag it along (#review)
|
||||||
|
void separatedWireKeepsVoltageWhenNotEdited()
|
||||||
|
{
|
||||||
|
FakeWire a("1", "fnA", "", {}), c("3", "fnC", "", {});
|
||||||
|
FakeWire::separate({&a}, {&c});
|
||||||
|
a.m_properties.m_tension_protocol = "24V";
|
||||||
|
c.m_properties.m_tension_protocol = "0V";
|
||||||
|
|
||||||
|
QSet<FakeWire *> separated;
|
||||||
|
const Wires t = ConductorMultiEdit::targets(Wires {&a}, true,
|
||||||
|
&separated);
|
||||||
|
const ConductorProperties shown = a.properties();
|
||||||
|
QUndoStack stack;
|
||||||
|
|
||||||
|
//Only the function changes: the far side changes nothing
|
||||||
|
stack.push(ConductorMultiEdit::undo(t, shown, edited(shown,
|
||||||
|
&ConductorProperties::m_function, "PWR"), separated));
|
||||||
|
QCOMPARE(c.m_properties.m_function, QStringLiteral("fnC"));
|
||||||
|
QCOMPARE(c.m_properties.m_tension_protocol, QStringLiteral("0V"));
|
||||||
|
|
||||||
|
//Now the voltage changes too: it crosses, and only it
|
||||||
|
const ConductorProperties shown_now = a.properties();
|
||||||
|
stack.push(ConductorMultiEdit::undo(t, shown_now, edited(shown_now,
|
||||||
|
&ConductorProperties::m_tension_protocol, "30V"), separated));
|
||||||
|
QCOMPARE(c.m_properties.m_tension_protocol, QStringLiteral("30V"));
|
||||||
|
QCOMPARE(c.m_properties.m_function, QStringLiteral("fnC"));
|
||||||
|
QCOMPARE(a.m_properties.m_tension_protocol, QStringLiteral("30V"));
|
||||||
|
}
|
||||||
|
|
||||||
// One edit of three wires: each takes the function, keeps its
|
// One edit of three wires: each takes the function, keeps its
|
||||||
// own number and cable; one undo restores all three exactly
|
// own number and cable; one undo restores all three exactly
|
||||||
void undoRedoThreeWires()
|
void undoRedoThreeWires()
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2006-2026 The QElectroTech Team
|
||||||
|
This file is part of QElectroTech.
|
||||||
|
|
||||||
|
QElectroTech is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
QElectroTech is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#include "qetapptest.h"
|
||||||
|
#include "diagramcontent.h"
|
||||||
|
#include "qetgraphicsitem/diagramimageitem.h"
|
||||||
|
|
||||||
|
#include <QDomDocument>
|
||||||
|
#include <QRandomGenerator>
|
||||||
|
|
||||||
|
/**
|
||||||
|
Diagram::fromXml() decodes a folio's pictures in parallel, then builds
|
||||||
|
the items in file order. Every picture must come back with its own
|
||||||
|
pixels and position, whatever thread decoded it.
|
||||||
|
*/
|
||||||
|
class tst_folioimageload : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
Diagram *m_diagram = nullptr;
|
||||||
|
|
||||||
|
/// A picture no other one equals: its own size and noise.
|
||||||
|
static QImage distinctImage(int n)
|
||||||
|
{
|
||||||
|
QImage image(40 + 7 * n, 30 + 3 * n, QImage::Format_ARGB32);
|
||||||
|
QRandomGenerator random(quint32(n + 1));
|
||||||
|
for (int y = 0 ; y < image.height() ; ++y)
|
||||||
|
for (int x = 0 ; x < image.width() ; ++x)
|
||||||
|
image.setPixel(x, y, random.generate() | 0xFF000000);
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QImage pixelsOf(const DiagramImageItem *item)
|
||||||
|
{
|
||||||
|
return item->pixmap().toImage()
|
||||||
|
.convertToFormat(QImage::Format_ARGB32);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removePictures()
|
||||||
|
{
|
||||||
|
// A selected picture's handles are its children, in items()
|
||||||
|
// too: deleting the picture first would leave them dangling.
|
||||||
|
m_diagram->clearSelection();
|
||||||
|
for (QGraphicsItem *item : m_diagram->items())
|
||||||
|
if (item->type() == DiagramImageItem::Type)
|
||||||
|
delete item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void initTestCase()
|
||||||
|
{
|
||||||
|
QVERIFY(QetAppTest::editor());
|
||||||
|
m_diagram = QetAppTest::diagram();
|
||||||
|
QVERIFY(m_diagram);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cleanup()
|
||||||
|
{
|
||||||
|
m_diagram->clearSelection();
|
||||||
|
removePictures();
|
||||||
|
m_diagram->undoStack().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void everyPictureComesBackWithItsPixels()
|
||||||
|
{
|
||||||
|
QHash<int, QImage> expected; // by x position, each its own
|
||||||
|
for (int n = 0 ; n < 6 ; ++n) {
|
||||||
|
const QImage image = distinctImage(n);
|
||||||
|
auto *picture = new DiagramImageItem(QPixmap::fromImage(image));
|
||||||
|
m_diagram->addItem(picture);
|
||||||
|
picture->setPos(100 + 150 * n, 200);
|
||||||
|
picture->setSelected(true);
|
||||||
|
expected.insert(qRound(picture->pos().x()), image);
|
||||||
|
}
|
||||||
|
QDomDocument document = m_diagram->toXml(false);
|
||||||
|
removePictures();
|
||||||
|
|
||||||
|
DiagramContent content;
|
||||||
|
QVERIFY(m_diagram->fromXml(document, QPointF(), false, &content));
|
||||||
|
|
||||||
|
QCOMPARE(content.m_images.size(), expected.size());
|
||||||
|
for (const DiagramImageItem *picture : std::as_const(content.m_images)) {
|
||||||
|
const int x = qRound(picture->pos().x());
|
||||||
|
QVERIFY2(expected.contains(x), "picture moved");
|
||||||
|
QCOMPARE(picture->pos().y(), 200.0);
|
||||||
|
QCOMPARE(pixelsOf(picture), expected.value(x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void anImageWithoutPictureDoesNotStopTheOthers()
|
||||||
|
{
|
||||||
|
for (int n = 0 ; n < 3 ; ++n) {
|
||||||
|
auto *picture = new DiagramImageItem(
|
||||||
|
QPixmap::fromImage(distinctImage(n)));
|
||||||
|
m_diagram->addItem(picture);
|
||||||
|
picture->setSelected(true);
|
||||||
|
}
|
||||||
|
QDomDocument document = m_diagram->toXml(false);
|
||||||
|
removePictures();
|
||||||
|
QDomElement images = document.documentElement()
|
||||||
|
.firstChildElement(QStringLiteral("images"));
|
||||||
|
QVERIFY(!images.isNull());
|
||||||
|
QDomElement empty = document.createElement(QStringLiteral("image"));
|
||||||
|
images.insertBefore(empty, images.firstChild());
|
||||||
|
|
||||||
|
DiagramContent content;
|
||||||
|
QVERIFY(m_diagram->fromXml(document, QPointF(), false, &content));
|
||||||
|
|
||||||
|
int with_pixels = 0;
|
||||||
|
for (const DiagramImageItem *picture : std::as_const(content.m_images))
|
||||||
|
if (!picture->pixmap().isNull())
|
||||||
|
++with_pixels;
|
||||||
|
QCOMPARE(with_pixels, 3);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QET_APP_TEST_MAIN(tst_folioimageload)
|
||||||
|
#include "tst_folioimageload.moc"
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#include "qetapptest.h"
|
||||||
|
#include "print/projectprintwindow.h"
|
||||||
|
#include "qetproject.h"
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QPrinter>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QTemporaryDir>
|
||||||
|
|
||||||
|
// The print window's PDF export turns each page to its folio (#1480). It
|
||||||
|
// used one orientation for the whole document, landscape unless changed,
|
||||||
|
// so a portrait folio was shrunk onto a landscape page.
|
||||||
|
// fixtures/pdf_page_a3.qet has an A3 landscape folio, then an A3 portrait
|
||||||
|
// one.
|
||||||
|
class tst_printpdforientation : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
QTemporaryDir m_dir;
|
||||||
|
|
||||||
|
struct Page
|
||||||
|
{
|
||||||
|
QSizeF size;
|
||||||
|
QList<QRectF> links;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Each page's MediaBox and the Rect of each of its links, in page
|
||||||
|
// order. Qt writes every object uncompressed, a page's annotations as
|
||||||
|
// "/Annots n 0 R" pointing at an array of annotation references.
|
||||||
|
static QList<Page> pages(const QString &pdf_path)
|
||||||
|
{
|
||||||
|
QFile file(pdf_path);
|
||||||
|
if (!file.open(QIODevice::ReadOnly))
|
||||||
|
return {};
|
||||||
|
const QString pdf = QString::fromLatin1(file.readAll());
|
||||||
|
|
||||||
|
QHash<int, QString> objects;
|
||||||
|
static const QRegularExpression object(
|
||||||
|
QStringLiteral(R"((\d+) 0 obj(.*?)endobj)"),
|
||||||
|
QRegularExpression::DotMatchesEverythingOption);
|
||||||
|
for (auto it = object.globalMatch(pdf); it.hasNext();) {
|
||||||
|
const auto m = it.next();
|
||||||
|
objects.insert(m.captured(1).toInt(), m.captured(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
static const QRegularExpression ref(QStringLiteral(R"((\d+) 0 R)"));
|
||||||
|
static const QRegularExpression kids(
|
||||||
|
QStringLiteral(R"(/Type\s*/Pages.*?/Kids\s*\[(.*?)\])"),
|
||||||
|
QRegularExpression::DotMatchesEverythingOption);
|
||||||
|
static const QRegularExpression box(
|
||||||
|
QStringLiteral(R"(/MediaBox \[0 0 ([0-9.]+) ([0-9.]+)\])"));
|
||||||
|
static const QRegularExpression annots(QStringLiteral(R"(/Annots (\d+) 0 R)"));
|
||||||
|
static const QRegularExpression link(
|
||||||
|
QStringLiteral(R"(/Subtype /Link\s*/Rect \[\s*([-0-9.]+)\s+([-0-9.]+)\s+([-0-9.]+)\s+([-0-9.]+)\s*\])"));
|
||||||
|
|
||||||
|
QList<Page> result;
|
||||||
|
for (auto k = ref.globalMatch(kids.match(pdf).captured(1)); k.hasNext();) {
|
||||||
|
const QString page = objects.value(k.next().captured(1).toInt());
|
||||||
|
const auto b = box.match(page);
|
||||||
|
Page p;
|
||||||
|
p.size = QSizeF(b.captured(1).toDouble(), b.captured(2).toDouble());
|
||||||
|
const QString list = objects.value(annots.match(page).captured(1).toInt());
|
||||||
|
for (auto a = ref.globalMatch(list); a.hasNext();) {
|
||||||
|
const auto l = link.match(objects.value(a.next().captured(1).toInt()));
|
||||||
|
if (l.hasMatch())
|
||||||
|
p.links << QRectF(QPointF(l.captured(1).toDouble(), l.captured(2).toDouble()),
|
||||||
|
QPointF(l.captured(3).toDouble(), l.captured(4).toDouble()));
|
||||||
|
}
|
||||||
|
result << p;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export @p project as the window's "Export in pdf" button does, from
|
||||||
|
// an A3 landscape printer with fixed margins.
|
||||||
|
QList<Page> exportPdf(QETProject *project, bool fit_in_page)
|
||||||
|
{
|
||||||
|
const QString out = m_dir.filePath(QStringLiteral("out.pdf"));
|
||||||
|
QFile::remove(out);
|
||||||
|
QSettings().setValue("print/default/fitinpage", fit_in_page);
|
||||||
|
|
||||||
|
auto printer = new QPrinter();
|
||||||
|
printer->setPageSize(QPageSize(QPageSize::A3));
|
||||||
|
printer->setPageOrientation(QPageLayout::Landscape);
|
||||||
|
printer->setOutputFormat(QPrinter::PdfFormat);
|
||||||
|
printer->setOutputFileName(out);
|
||||||
|
|
||||||
|
ProjectPrintWindow window(project, printer);
|
||||||
|
// After the window loaded the margins an earlier export saved.
|
||||||
|
printer->setPageMargins(QMarginsF(7, 7, 7, 7), QPageLayout::Millimeter);
|
||||||
|
if (!QMetaObject::invokeMethod(&window, "print"))
|
||||||
|
return {};
|
||||||
|
QCoreApplication::processEvents(); // the deferred PDF post-processing
|
||||||
|
return pages(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// examples/industrial.qet, its folios made portrait (taller rows), all
|
||||||
|
// of them or all but the first.
|
||||||
|
QString portraitIndustrial(bool first_too)
|
||||||
|
{
|
||||||
|
QFile file(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
|
||||||
|
if (!file.open(QIODevice::ReadOnly))
|
||||||
|
return {};
|
||||||
|
QString xml = QString::fromUtf8(file.readAll());
|
||||||
|
static const QRegularExpression diagram(QStringLiteral("<diagram [^>]*>"));
|
||||||
|
static const QRegularExpression rows(QStringLiteral(R"(\brows="\d+")"));
|
||||||
|
QString result;
|
||||||
|
qsizetype last = 0;
|
||||||
|
int n = 0;
|
||||||
|
for (auto it = diagram.globalMatch(xml); it.hasNext();) {
|
||||||
|
const auto m = it.next();
|
||||||
|
QString tag = m.captured();
|
||||||
|
if (n++ > 0 || first_too)
|
||||||
|
tag.replace(rows, QStringLiteral("rows=\"20\""));
|
||||||
|
result += xml.mid(last, m.capturedStart() - last) + tag;
|
||||||
|
last = m.capturedEnd();
|
||||||
|
}
|
||||||
|
result += xml.mid(last);
|
||||||
|
|
||||||
|
const QString path = m_dir.filePath(
|
||||||
|
first_too ? QStringLiteral("portrait.qet") : QStringLiteral("mixed.qet"));
|
||||||
|
QFile out(path);
|
||||||
|
if (!out.open(QIODevice::WriteOnly))
|
||||||
|
return {};
|
||||||
|
out.write(result.toUtf8());
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void initTestCase()
|
||||||
|
{
|
||||||
|
QVERIFY(m_dir.isValid());
|
||||||
|
}
|
||||||
|
|
||||||
|
void pagesFollowFolios()
|
||||||
|
{
|
||||||
|
const QString fixture = QFINDTESTDATA("fixtures/pdf_page_a3.qet");
|
||||||
|
QVERIFY2(!fixture.isEmpty(), "fixture project not found");
|
||||||
|
QETProject project(fixture);
|
||||||
|
QCOMPARE(project.state(), QETProject::Ok);
|
||||||
|
QCOMPARE(project.diagrams().size(), 2);
|
||||||
|
|
||||||
|
const QList<Page> result = exportPdf(&project, true);
|
||||||
|
QCOMPARE(result.size(), 2);
|
||||||
|
const QSize a3 = QPageSize(QPageSize::A3).sizePoints();
|
||||||
|
QCOMPARE(result.at(0).size, QSizeF(a3.height(), a3.width())); // landscape
|
||||||
|
QCOMPARE(result.at(1).size, QSizeF(a3)); // portrait
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without "Fit in page" a folio is spread over several pages, all in the
|
||||||
|
// orientation chosen in the window, as before.
|
||||||
|
void tiledPagesKeepChosenOrientation()
|
||||||
|
{
|
||||||
|
const QString fixture = QFINDTESTDATA("fixtures/pdf_page_a3.qet");
|
||||||
|
QETProject project(fixture);
|
||||||
|
QCOMPARE(project.state(), QETProject::Ok);
|
||||||
|
|
||||||
|
const QList<Page> result = exportPdf(&project, false);
|
||||||
|
QVERIFY(!result.isEmpty());
|
||||||
|
for (const Page &page : result)
|
||||||
|
QVERIFY2(page.size.width() > page.size.height(), "a tiled page is not landscape");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cross-reference links on a page turned during the export land
|
||||||
|
// where they do when the export starts on that orientation: the same
|
||||||
|
// project with every folio portrait.
|
||||||
|
void linksOnTurnedPages()
|
||||||
|
{
|
||||||
|
const QString mixed_path = portraitIndustrial(false);
|
||||||
|
const QString portrait_path = portraitIndustrial(true);
|
||||||
|
QVERIFY(!mixed_path.isEmpty() && !portrait_path.isEmpty());
|
||||||
|
|
||||||
|
QETProject mixed_project(mixed_path);
|
||||||
|
QCOMPARE(mixed_project.state(), QETProject::Ok);
|
||||||
|
const QList<Page> mixed = exportPdf(&mixed_project, true);
|
||||||
|
QETProject portrait_project(portrait_path);
|
||||||
|
QCOMPARE(portrait_project.state(), QETProject::Ok);
|
||||||
|
const QList<Page> portrait = exportPdf(&portrait_project, true);
|
||||||
|
|
||||||
|
QCOMPARE(mixed.size(), portrait.size());
|
||||||
|
QVERIFY(mixed.size() > 1);
|
||||||
|
QVERIFY(mixed.first().size.width() > mixed.first().size.height());
|
||||||
|
int links = 0;
|
||||||
|
for (int i = 1; i < mixed.size(); ++i) {
|
||||||
|
QVERIFY(mixed.at(i).size.height() > mixed.at(i).size.width());
|
||||||
|
QCOMPARE(mixed.at(i).links.size(), portrait.at(i).links.size());
|
||||||
|
for (int j = 0; j < mixed.at(i).links.size(); ++j) {
|
||||||
|
const QRectF a = mixed.at(i).links.at(j);
|
||||||
|
const QRectF b = portrait.at(i).links.at(j);
|
||||||
|
QVERIFY2(qAbs(a.left() - b.left()) < 0.01 && qAbs(a.top() - b.top()) < 0.01
|
||||||
|
&& qAbs(a.right() - b.right()) < 0.01
|
||||||
|
&& qAbs(a.bottom() - b.bottom()) < 0.01,
|
||||||
|
qPrintable(QStringLiteral("page %1 link %2 moved").arg(i + 1).arg(j)));
|
||||||
|
++links;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY2(links > 100, "too few links to test");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QET_APP_TEST_MAIN(tst_printpdforientation)
|
||||||
|
#include "tst_printpdforientation.moc"
|
||||||
@@ -69,6 +69,70 @@ class tst_qetcontainer : public QObject
|
|||||||
return QetContainer::split(*original, QStringLiteral("tst_qetcontainer"), &error);
|
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)
|
||||||
|
if (e.name == name) return e.data;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static QByteArray folioFiles(const QList<QetZip::Entry> &entries)
|
||||||
|
{
|
||||||
|
QByteArray all;
|
||||||
|
for (const QetZip::Entry &e : entries)
|
||||||
|
if (e.name.startsWith(QStringLiteral("folios/"))) all += e.data;
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first column of the first row @p sql gives, from @p database
|
||||||
|
static QString query(const QByteArray &database, const char *sql)
|
||||||
|
{
|
||||||
|
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 *st = nullptr;
|
||||||
|
QString out;
|
||||||
|
if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW)
|
||||||
|
out = QString::fromUtf8(reinterpret_cast<const char *>(sqlite3_column_text(st, 0)));
|
||||||
|
sqlite3_finalize(st);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// grafcet.qet as QElectroTech saves it: every folio, symbol and wire
|
||||||
|
// with a uuid of its own, which the examples' wires and folios lack
|
||||||
|
static QDomDocument savedProject()
|
||||||
|
{
|
||||||
|
QList<QetZip::Entry> entries;
|
||||||
|
QDomDocument project;
|
||||||
|
QetZip::fromBytes(read(QStringLiteral(QET_FIXTURES_DIR "/qetz_format2.qetz")), &entries);
|
||||||
|
QetContainer::join(entries, &project);
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString backDifference(const QDomDocument &original, const QList<QetZip::Entry> &entries)
|
||||||
|
{
|
||||||
|
QDomDocument back;
|
||||||
|
QString error;
|
||||||
|
if (!QetContainer::join(entries, &back, &error)) return error;
|
||||||
|
return difference(withoutComputed(original).documentElement(), back.documentElement(), QString());
|
||||||
|
}
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void sameDocumentBack_data()
|
void sameDocumentBack_data()
|
||||||
{
|
{
|
||||||
@@ -100,7 +164,8 @@ private slots:
|
|||||||
QVERIFY2(QetZip::fromBytes(QetZip::toBytes(entries, &error), &unzipped, &error), qPrintable(error));
|
QVERIFY2(QetZip::fromBytes(QetZip::toBytes(entries, &error), &unzipped, &error), qPrintable(error));
|
||||||
QDomDocument back;
|
QDomDocument back;
|
||||||
QVERIFY2(QetContainer::join(unzipped, &back, &error), qPrintable(error));
|
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));
|
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +201,9 @@ private slots:
|
|||||||
|
|
||||||
// The engineering data is in project.sqlite and nowhere else: the folio
|
// The engineering data is in project.sqlite and nowhere else: the folio
|
||||||
// files hold no symbol information, links, wire numbers or title-block
|
// 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()
|
void databaseHoldsTheData()
|
||||||
{
|
{
|
||||||
QDomDocument original;
|
QDomDocument original;
|
||||||
@@ -181,6 +248,37 @@ private slots:
|
|||||||
sqlite3_close(db);
|
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:
|
// Terminal strips move into the database, and come back exactly:
|
||||||
// industrial.qet with two strips on its own terminal symbols, parsed as
|
// industrial.qet with two strips on its own terminal symbols, parsed as
|
||||||
// a save leaves it (no whitespace text).
|
// a save leaves it (no whitespace text).
|
||||||
@@ -217,7 +315,8 @@ private slots:
|
|||||||
QVERIFY2(!e.data.contains("<terminal_strip>"), "the strips stayed in project.xml");
|
QVERIFY2(!e.data.contains("<terminal_strip>"), "the strips stayed in project.xml");
|
||||||
QDomDocument back;
|
QDomDocument back;
|
||||||
QVERIFY2(QetContainer::join(entries, &back, &error), qPrintable(error));
|
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));
|
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +341,148 @@ private slots:
|
|||||||
QVERIFY(pictures < 121); // the 121 placed pictures repeat a few files
|
QVERIFY(pictures < 121); // the 121 placed pictures repeat a few files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Given a map, join() hands the pictures over as bytes instead of
|
||||||
|
// putting them back in the document as base64
|
||||||
|
void picturesKeptAside()
|
||||||
|
{
|
||||||
|
QDomDocument original;
|
||||||
|
QList<QetZip::Entry> entries = splitFile(
|
||||||
|
QStringLiteral(QET_EXAMPLES_DIR "/weneedpolonez-Polonez_MR89_wiring_diagram.qet"), &original);
|
||||||
|
QVERIFY(!entries.isEmpty());
|
||||||
|
QHash<QString, QByteArray> files;
|
||||||
|
for (const QetZip::Entry &e : entries) files.insert(e.name, e.data);
|
||||||
|
|
||||||
|
QDomDocument back;
|
||||||
|
QetContainer::Pictures pictures;
|
||||||
|
QString 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);
|
||||||
|
QCOMPARE(images.size(), original_images.size());
|
||||||
|
for (int i = 0 ; i < images.size() ; ++i) {
|
||||||
|
const QDomElement image = images.at(i).toElement();
|
||||||
|
const QString file = QetContainer::pictureFile(image);
|
||||||
|
QVERIFY(!file.isEmpty());
|
||||||
|
QVERIFY(!image.firstChild().isText());
|
||||||
|
QCOMPARE(pictures.value(file), files.value(file));
|
||||||
|
QCOMPARE(pictures.value(file),
|
||||||
|
QByteArray::fromBase64(original_images.at(i).firstChild().toText().data().toLatin1()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// still refused when a picture is missing
|
||||||
|
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, nullptr, &pictures));
|
||||||
|
QVERIFY2(error.contains(QStringLiteral("missing")), qPrintable(error));
|
||||||
|
}
|
||||||
|
|
||||||
// A file whose manifest needs a later reader is refused, not half read
|
// A file whose manifest needs a later reader is refused, not half read
|
||||||
|
// A later format whose min-reader allows this one opens, and join() says
|
||||||
|
// it is later: QElectroTech opens it read-only
|
||||||
|
void laterFormatAllowedRead()
|
||||||
|
{
|
||||||
|
QDomDocument original;
|
||||||
|
QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), &original);
|
||||||
|
QVERIFY(!entries.isEmpty());
|
||||||
|
const int later = QetContainer::Format + 1;
|
||||||
|
for (QetZip::Entry &e : entries)
|
||||||
|
if (e.name == QLatin1String("manifest.xml"))
|
||||||
|
e.data = QString::fromUtf8(e.data).replace(QRegularExpression(QStringLiteral(" format=\"\\d+\"")),
|
||||||
|
QStringLiteral(" format=\"%1\"").arg(later)).toUtf8();
|
||||||
|
alterDatabase(entries, QStringLiteral("UPDATE meta SET value = '%1' WHERE key = 'format'")
|
||||||
|
.arg(later).toUtf8().constData());
|
||||||
|
QDomDocument back;
|
||||||
|
QString error;
|
||||||
|
int format = 0;
|
||||||
|
QVERIFY2(QetContainer::join(entries, &back, &error, nullptr, nullptr, &format), qPrintable(error));
|
||||||
|
QCOMPARE(format, later);
|
||||||
|
const QString d = difference(withoutComputed(original).documentElement(), back.documentElement(), QString());
|
||||||
|
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The database holds data, not where a block sat in the XML: the folio
|
||||||
|
// file's marker says where it goes back
|
||||||
|
void noPositionColumns()
|
||||||
|
{
|
||||||
|
QDomDocument original;
|
||||||
|
const QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), &original);
|
||||||
|
const QByteArray database = entry(entries, QStringLiteral("project.sqlite"));
|
||||||
|
QCOMPARE(query(database, "SELECT group_concat(name) FROM pragma_table_info('element')"),
|
||||||
|
QStringLiteral("uuid,folio_uuid,prefix,freezeLabel,info_attrs,links_attrs"));
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM pragma_table_info('folio') WHERE name LIKE '%_at'"),
|
||||||
|
QStringLiteral("0"));
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM meta WHERE key = 'props_at'"), QStringLiteral("0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A project as QElectroTech saves it needs no JSON: every attribute it
|
||||||
|
// writes has a column, the show flag of a property too
|
||||||
|
void savedProjectNeedsNoJson()
|
||||||
|
{
|
||||||
|
QString error;
|
||||||
|
const QList<QetZip::Entry> entries = QetContainer::split(savedProject(), QString(), &error);
|
||||||
|
QVERIFY2(!entries.isEmpty(), qPrintable(error));
|
||||||
|
const QByteArray database = entry(entries, QStringLiteral("project.sqlite"));
|
||||||
|
QVERIFY(query(database, "SELECT count(*) FROM project_property").toInt() > 0);
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM project_property WHERE show IS NOT NULL"),
|
||||||
|
query(database, "SELECT count(*) FROM project_property"));
|
||||||
|
for (const char *sql : {"SELECT count(*) FROM project_property WHERE attrs IS NOT NULL",
|
||||||
|
"SELECT count(*) FROM folio_property WHERE attrs IS NOT NULL",
|
||||||
|
"SELECT count(*) FROM element_info WHERE attrs IS NOT NULL",
|
||||||
|
"SELECT count(*) FROM link WHERE attrs IS NOT NULL",
|
||||||
|
"SELECT count(*) FROM folio WHERE props_attrs IS NOT NULL",
|
||||||
|
"SELECT count(*) FROM element WHERE info_attrs IS NOT NULL OR links_attrs IS NOT NULL"})
|
||||||
|
QVERIFY2(query(database, sql) == QLatin1String("0"), sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The schema in QETZ-FORMAT.md is the one written: same tables, same
|
||||||
|
// columns in the same order, same types; written without keys (format 4)
|
||||||
|
void specMatchesSchema()
|
||||||
|
{
|
||||||
|
const QString spec = QString::fromUtf8(read(QStringLiteral(QET_FORMAT_SPEC)));
|
||||||
|
const int from = spec.indexOf(QStringLiteral("```sql\n"));
|
||||||
|
const int to = spec.indexOf(QStringLiteral("```"), from + 7);
|
||||||
|
QVERIFY2(from >= 0 && to > from, "no schema in QETZ-FORMAT.md");
|
||||||
|
const QByteArray sql = spec.mid(from + 7, to - from - 7).toUtf8();
|
||||||
|
|
||||||
|
QDomDocument original;
|
||||||
|
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, const char *columns = "m.name, p.name, p.type") {
|
||||||
|
QStringList out;
|
||||||
|
sqlite3_stmt *st = nullptr;
|
||||||
|
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 < sqlite3_column_count(st) ; ++i)
|
||||||
|
row << QString::fromUtf8(reinterpret_cast<const char *>(sqlite3_column_text(st, i)));
|
||||||
|
out << row.join(QLatin1Char(' '));
|
||||||
|
}
|
||||||
|
sqlite3_finalize(st);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
sqlite3 *from_spec = nullptr;
|
||||||
|
sqlite3_open(":memory:", &from_spec);
|
||||||
|
QCOMPARE(sqlite3_exec(from_spec, sql.constData(), nullptr, nullptr, nullptr), SQLITE_OK);
|
||||||
|
sqlite3 *from_file = nullptr;
|
||||||
|
sqlite3_open(":memory:", &from_file);
|
||||||
|
unsigned char *copy = static_cast<unsigned char *>(sqlite3_malloc64(sqlite3_uint64(written.size())));
|
||||||
|
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());
|
||||||
|
QCOMPARE(spec_tables, file_tables);
|
||||||
|
}
|
||||||
|
|
||||||
void laterFormatRefused()
|
void laterFormatRefused()
|
||||||
{
|
{
|
||||||
QDomDocument original;
|
QDomDocument original;
|
||||||
@@ -272,6 +512,108 @@ private slots:
|
|||||||
|
|
||||||
// A crafted project.sqlite whose "element" is a view, not a table, is
|
// A crafted project.sqlite whose "element" is a view, not a table, is
|
||||||
// refused: nothing in a file's schema is trusted.
|
// 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)
|
||||||
|
{
|
||||||
|
for (QetZip::Entry &e : entries) {
|
||||||
|
if (e.name != QLatin1String("project.sqlite")) continue;
|
||||||
|
sqlite3 *db = nullptr;
|
||||||
|
sqlite3_open(":memory:", &db);
|
||||||
|
unsigned char *copy = static_cast<unsigned char *>(sqlite3_malloc64(sqlite3_uint64(e.data.size())));
|
||||||
|
memcpy(copy, e.data.constData(), size_t(e.data.size()));
|
||||||
|
sqlite3_deserialize(db, "main", copy, e.data.size(), e.data.size(),
|
||||||
|
SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_RESIZEABLE);
|
||||||
|
QCOMPARE(sqlite3_exec(db, sql, nullptr, nullptr, nullptr), SQLITE_OK);
|
||||||
|
sqlite3_int64 size = 0;
|
||||||
|
unsigned char *bytes = sqlite3_serialize(db, "main", &size, 0);
|
||||||
|
e.data = QByteArray(reinterpret_cast<const char *>(bytes), qsizetype(size));
|
||||||
|
sqlite3_free(bytes);
|
||||||
|
sqlite3_close(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A database that does not match its folio files -- a row no item has,
|
||||||
|
// an item's row on another folio, entries of no symbol -- is refused
|
||||||
|
// with what is wrong, not read as far as it goes
|
||||||
|
void mismatchRefused_data()
|
||||||
|
{
|
||||||
|
QTest::addColumn<QByteArray>("sql");
|
||||||
|
QTest::addColumn<QString>("said");
|
||||||
|
QTest::newRow("symbol row of no symbol")
|
||||||
|
<< 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,"
|
||||||
|
" bus, freezeLabel FROM conductor LIMIT 1")
|
||||||
|
<< QStringLiteral("wire rows");
|
||||||
|
QTest::newRow("symbol on another folio")
|
||||||
|
<< QByteArray("UPDATE element SET folio_uuid = (SELECT uuid FROM folio WHERE pos = 2)"
|
||||||
|
" WHERE folio_uuid = (SELECT uuid FROM folio WHERE pos = 1)")
|
||||||
|
<< QStringLiteral("the database says another");
|
||||||
|
QTest::newRow("information of no symbol")
|
||||||
|
<< QByteArray("UPDATE element_info SET element_uuid = 'x' WHERE rowid = 1")
|
||||||
|
<< QStringLiteral("information rows of no symbol");
|
||||||
|
}
|
||||||
|
void mismatchRefused()
|
||||||
|
{
|
||||||
|
QFETCH(QByteArray, sql);
|
||||||
|
QFETCH(QString, said);
|
||||||
|
QDomDocument original;
|
||||||
|
QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), &original);
|
||||||
|
QVERIFY(!entries.isEmpty());
|
||||||
|
{
|
||||||
|
QDomDocument back;
|
||||||
|
QString error;
|
||||||
|
QVERIFY2(QetContainer::join(entries, &back, &error), qPrintable(error));
|
||||||
|
}
|
||||||
|
alterDatabase(entries, sql.constData());
|
||||||
|
QDomDocument back;
|
||||||
|
QString error;
|
||||||
|
QVERIFY(!QetContainer::join(entries, &back, &error));
|
||||||
|
QVERIFY2(error.contains(said), qPrintable(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A folio file says which symbol each item is (its type), read alone
|
||||||
|
void folioSaysWhichSymbol()
|
||||||
|
{
|
||||||
|
QDomDocument original;
|
||||||
|
const QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), &original);
|
||||||
|
QVERIFY(!entries.isEmpty());
|
||||||
|
int typed = 0, symbols = 0;
|
||||||
|
for (const QetZip::Entry &e : entries) {
|
||||||
|
if (!e.name.startsWith(QStringLiteral("folios/"))) continue;
|
||||||
|
const QString text = QString::fromUtf8(e.data);
|
||||||
|
symbols += int(text.count(QStringLiteral("<element ")));
|
||||||
|
typed += int(text.count(QRegularExpression(QStringLiteral("<element [^>]*type=\"embed://"))));
|
||||||
|
}
|
||||||
|
QVERIFY(symbols > 0);
|
||||||
|
QCOMPARE(typed, symbols);
|
||||||
|
}
|
||||||
|
|
||||||
void viewInsteadOfTableRefused()
|
void viewInsteadOfTableRefused()
|
||||||
{
|
{
|
||||||
QDomDocument original;
|
QDomDocument original;
|
||||||
@@ -300,6 +642,104 @@ private slots:
|
|||||||
QVERIFY2(error.contains(QStringLiteral("element")), qPrintable(error));
|
QVERIFY2(error.contains(QStringLiteral("element")), qPrintable(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A row's key is its item's uuid, which the folio file keeps: no key
|
||||||
|
// made up from where an item is (format 2 had f1, f2...), no key
|
||||||
|
// attribute in the folio files of a project as QElectroTech saves it,
|
||||||
|
// true/false held as 1/0, and the format in the database's
|
||||||
|
// user_version.
|
||||||
|
void keysAreTheUuids()
|
||||||
|
{
|
||||||
|
const QDomDocument original = savedProject();
|
||||||
|
original.elementsByTagName(QStringLiteral("conductor")).at(0).toElement()
|
||||||
|
.setAttribute(QStringLiteral("freezeLabel"), QStringLiteral("true"));
|
||||||
|
QString error;
|
||||||
|
const QList<QetZip::Entry> entries = QetContainer::split(original, QString(), &error);
|
||||||
|
QVERIFY2(!entries.isEmpty(), qPrintable(error));
|
||||||
|
const QByteArray database = entry(entries, QStringLiteral("project.sqlite"));
|
||||||
|
const QByteArray folios = folioFiles(entries);
|
||||||
|
QVERIFY(!folios.contains("qetz-key"));
|
||||||
|
QVERIFY(!folios.contains("qetz-folio"));
|
||||||
|
QCOMPARE(query(database, "PRAGMA user_version"), QString::number(QetContainer::Format));
|
||||||
|
const QString first = original.elementsByTagName(QStringLiteral("diagram")).at(0).toElement()
|
||||||
|
.attribute(QStringLiteral("uuid"));
|
||||||
|
QCOMPARE(query(database, "SELECT uuid FROM folio WHERE pos = 1"), first);
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM folio WHERE uuid NOT LIKE '{%}'"), QStringLiteral("0"));
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM element WHERE uuid NOT LIKE '{%}'"), QStringLiteral("0"));
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM conductor WHERE uuid NOT LIKE '{%}'"), QStringLiteral("0"));
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM element WHERE typeof(freezeLabel) <> 'integer'"),
|
||||||
|
QStringLiteral("0"));
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM conductor WHERE typeof(freezeLabel) <> 'integer'"),
|
||||||
|
QStringLiteral("0"));
|
||||||
|
QCOMPARE(query(database, "SELECT sum(freezeLabel) FROM conductor"), QStringLiteral("1"));
|
||||||
|
const QString d = backDifference(original, entries);
|
||||||
|
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM element_info WHERE typeof(show) <> 'integer'"),
|
||||||
|
QStringLiteral("0"));
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM element_info WHERE attrs LIKE '%\"tag\"%'"),
|
||||||
|
QStringLiteral("0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two symbols sharing a uuid (F100) each get a key made from what they
|
||||||
|
// hold, the same on every save, and come back as they were.
|
||||||
|
void sharedUuidKeyed()
|
||||||
|
{
|
||||||
|
QDomDocument original = savedProject();
|
||||||
|
const QDomNodeList elements = original.elementsByTagName(QStringLiteral("element"));
|
||||||
|
QDomElement a, b;
|
||||||
|
for (int i = 0 ; i < elements.size() && b.isNull() ; ++i) {
|
||||||
|
const QDomElement e = elements.at(i).toElement();
|
||||||
|
if (e.parentNode().nodeName() != QLatin1String("elements")) continue;
|
||||||
|
if (a.isNull()) a = e; else b = e;
|
||||||
|
}
|
||||||
|
QVERIFY(!b.isNull());
|
||||||
|
b.setAttribute(QStringLiteral("uuid"), a.attribute(QStringLiteral("uuid")));
|
||||||
|
QString error;
|
||||||
|
const QList<QetZip::Entry> once = QetContainer::split(original, QString(), &error);
|
||||||
|
const QList<QetZip::Entry> twice = QetContainer::split(original, QString(), &error);
|
||||||
|
QVERIFY2(!once.isEmpty(), qPrintable(error));
|
||||||
|
QCOMPARE(folioFiles(once).count("qetz-key"), 2);
|
||||||
|
QCOMPARE(entry(once, QStringLiteral("project.sqlite")), entry(twice, QStringLiteral("project.sqlite")));
|
||||||
|
const QString d = backDifference(original, once);
|
||||||
|
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A true/false attribute QElectroTech would not write stays in the
|
||||||
|
// folio file as it was.
|
||||||
|
void oddBooleanKept()
|
||||||
|
{
|
||||||
|
QDomDocument original;
|
||||||
|
QVERIFY(QetContainer::parse(read(QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet")), &original));
|
||||||
|
const QDomNodeList conductors = original.elementsByTagName(QStringLiteral("conductor"));
|
||||||
|
QVERIFY(conductors.size() > 0);
|
||||||
|
conductors.at(0).toElement().setAttribute(QStringLiteral("freezeLabel"), QStringLiteral("yes"));
|
||||||
|
QString error;
|
||||||
|
const QList<QetZip::Entry> entries = QetContainer::split(original, QString(), &error);
|
||||||
|
QVERIFY2(!entries.isEmpty(), qPrintable(error));
|
||||||
|
QVERIFY(folioFiles(entries).contains("freezeLabel=\"yes\""));
|
||||||
|
const QString d = backDifference(original, entries);
|
||||||
|
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A format 2 file, as QElectroTech 0.200.1 development builds wrote it
|
||||||
|
// (grafcet.qet saved as .qetz), still opens, and saved again as the
|
||||||
|
// current format comes back the same.
|
||||||
|
void format2Read()
|
||||||
|
{
|
||||||
|
QList<QetZip::Entry> entries;
|
||||||
|
QString error;
|
||||||
|
QVERIFY2(QetZip::fromBytes(read(QStringLiteral(QET_FIXTURES_DIR "/qetz_format2.qetz")), &entries, &error),
|
||||||
|
qPrintable(error));
|
||||||
|
QVERIFY(entry(entries, QStringLiteral("manifest.xml")).contains("format=\"2\""));
|
||||||
|
QDomDocument project;
|
||||||
|
QVERIFY2(QetContainer::join(entries, &project, &error), qPrintable(error));
|
||||||
|
QVERIFY(project.toString().contains(QStringLiteral("<elementInformation ")));
|
||||||
|
QVERIFY(!project.toString().contains(QStringLiteral("qetz-")));
|
||||||
|
const QList<QetZip::Entry> resaved = QetContainer::split(project, QString(), &error);
|
||||||
|
QVERIFY2(!resaved.isEmpty(), qPrintable(error));
|
||||||
|
const QString d = backDifference(project, resaved);
|
||||||
|
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||||
|
}
|
||||||
|
|
||||||
void notAProjectRefused()
|
void notAProjectRefused()
|
||||||
{
|
{
|
||||||
QDomDocument back;
|
QDomDocument back;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include <QtTest>
|
#include <QtTest>
|
||||||
|
|
||||||
#include <QTemporaryDir>
|
#include <QTemporaryDir>
|
||||||
|
#include <QtEndian>
|
||||||
|
|
||||||
#include "container/qetzip.h"
|
#include "container/qetzip.h"
|
||||||
#include "container/miniz/miniz.h"
|
#include "container/miniz/miniz.h"
|
||||||
@@ -72,6 +73,33 @@ private slots:
|
|||||||
QCOMPARE(QetZip::toBytes(sample()), QetZip::toBytes(sample()));
|
QCOMPARE(QetZip::toBytes(sample()), QetZip::toBytes(sample()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// every entry carries a valid date in both of its headers, not the
|
||||||
|
// 0 miniz writes without a clock (shown as 1979 or 1901 by unzip tools)
|
||||||
|
void entriesHaveValidDate()
|
||||||
|
{
|
||||||
|
const QByteArray zip = QetZip::toBytes(sample());
|
||||||
|
QVERIFY(!zip.isEmpty());
|
||||||
|
mz_zip_archive archive;
|
||||||
|
mz_zip_zero_struct(&archive);
|
||||||
|
QVERIFY(mz_zip_reader_init_mem(&archive, zip.constData(), size_t(zip.size()), 0));
|
||||||
|
const mz_uint count = mz_zip_reader_get_num_files(&archive);
|
||||||
|
QCOMPARE(int(count), sample().size());
|
||||||
|
for (mz_uint i = 0 ; i < count ; ++i) {
|
||||||
|
mz_zip_archive_file_stat stat;
|
||||||
|
QVERIFY(mz_zip_reader_file_stat(&archive, i, &stat));
|
||||||
|
const char *local = zip.constData() + stat.m_local_header_ofs;
|
||||||
|
const char *central = zip.constData() + archive.m_central_directory_file_ofs + stat.m_central_dir_ofs;
|
||||||
|
QCOMPARE(qFromLittleEndian<quint16>(local), quint16(0x50 | 0x4b << 8)); // "PK"
|
||||||
|
QCOMPARE(qFromLittleEndian<quint16>(central), quint16(0x50 | 0x4b << 8));
|
||||||
|
// 1 Jan 1980: day 1, month 1, year 0 since 1980; time 00:00:00
|
||||||
|
QCOMPARE(qFromLittleEndian<quint16>(local + 12), quint16(0x21));
|
||||||
|
QCOMPARE(qFromLittleEndian<quint16>(central + 14), quint16(0x21));
|
||||||
|
QCOMPARE(qFromLittleEndian<quint16>(local + 10), quint16(0));
|
||||||
|
QCOMPARE(qFromLittleEndian<quint16>(central + 12), quint16(0));
|
||||||
|
}
|
||||||
|
mz_zip_reader_end(&archive);
|
||||||
|
}
|
||||||
|
|
||||||
void refusesUnsafeNamesOnWrite_data()
|
void refusesUnsafeNamesOnWrite_data()
|
||||||
{
|
{
|
||||||
QTest::addColumn<QString>("name");
|
QTest::addColumn<QString>("name");
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QTemporaryDir>
|
#include <QTemporaryDir>
|
||||||
|
|
||||||
|
#include "container/qetzip.h"
|
||||||
|
|
||||||
|
#include <sqlite3.h>
|
||||||
|
|
||||||
// Saving a project that was just saved must change nothing. Two things
|
// 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
|
// made the second save differ from the first, both cleanup done on save
|
||||||
// but not on load:
|
// but not on load:
|
||||||
@@ -306,6 +310,10 @@ private slots:
|
|||||||
QVERIFY2(!zipped.isEmpty(), "--resave as .qetz failed");
|
QVERIFY2(!zipped.isEmpty(), "--resave as .qetz failed");
|
||||||
QVERIFY2(read(zipped).startsWith("PK\x03\x04"), "the .qetz is not a zip");
|
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");
|
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);
|
const QString back = resave(zipped);
|
||||||
QVERIFY2(!back.isEmpty(), "opening the .qetz failed");
|
QVERIFY2(!back.isEmpty(), "opening the .qetz failed");
|
||||||
|
|
||||||
@@ -317,6 +325,31 @@ private slots:
|
|||||||
QCOMPARE(without_save_stamps(read(back)), without_save_stamps(read(straight)));
|
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
|
// QElectroTech run in a sandbox of its own with @p args, the store
|
||||||
// check on (QET_CHECK_ELEMENT_INFO_STORE); its output
|
// check on (QET_CHECK_ELEMENT_INFO_STORE); its output
|
||||||
QString runChecked(const QStringList &args, bool eager = false)
|
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_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||||
env.insert(QStringLiteral("QET_CHECK_ELEMENT_INFO_STORE"), QStringLiteral("1"));
|
env.insert(QStringLiteral("QET_CHECK_ELEMENT_INFO_STORE"), QStringLiteral("1"));
|
||||||
if (eager) {
|
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("HOME"), home);
|
||||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||||
@@ -340,8 +373,8 @@ private slots:
|
|||||||
return QString::fromUtf8(proc.readAllStandardOutput() + proc.readAllStandardError());
|
return QString::fromUtf8(proc.readAllStandardOutput() + proc.readAllStandardError());
|
||||||
}
|
}
|
||||||
|
|
||||||
// The stores of symbol information and folio title blocks
|
// The stores of symbol information and folio title blocks agree with
|
||||||
// (DB-ACCESSORS-PLAN.md stages 4.1, 4.4) agree with every placed symbol
|
// every placed symbol
|
||||||
// and folio once a project is open: every example.
|
// and folio once a project is open: every example.
|
||||||
void elementInfoStoreAgrees_data()
|
void elementInfoStoreAgrees_data()
|
||||||
{
|
{
|
||||||
@@ -361,7 +394,7 @@ private slots:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The stores filled from the document before the folios are built
|
// The stores filled from the document before the folios are built
|
||||||
// (DB-ACCESSORS-PLAN.md stage 4.6) hold what building them gives, for a
|
// hold what building them gives, for a
|
||||||
// project saved by this version (every symbol, folio and wire carries
|
// project saved by this version (every symbol, folio and wire carries
|
||||||
// a uuid): nothing differs, nothing is left unplaced.
|
// a uuid): nothing differs, nothing is left unplaced.
|
||||||
void storesFilledBeforeBuilding()
|
void storesFilledBeforeBuilding()
|
||||||
@@ -479,6 +512,40 @@ private slots:
|
|||||||
QCOMPARE(rowsOf(edited), rowsOf(reopened));
|
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
|
// 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.
|
// (#973), and a value with accents comes back as it went in.
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2006-2026 The QElectroTech Team
|
||||||
|
This file is part of QElectroTech.
|
||||||
|
|
||||||
|
QElectroTech is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
QElectroTech is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
#include "qetapptest.h"
|
||||||
|
#include "qetgraphicsitem/diagramimageitem.h"
|
||||||
|
|
||||||
|
#include <QBuffer>
|
||||||
|
#include <QDomDocument>
|
||||||
|
#include <QPicture>
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QSvgGenerator>
|
||||||
|
#include <QTemporaryDir>
|
||||||
|
|
||||||
|
/**
|
||||||
|
SVG export with "smaller pictures" (ExportProperties::svg_small_pictures,
|
||||||
|
--svg-small-pictures) stores each picture at four times the size it is
|
||||||
|
drawn at when that is smaller than the picture, and at full resolution
|
||||||
|
otherwise. fixtures/svg_small_pictures.qet has an 800x600 picture drawn
|
||||||
|
at a tenth of its size and a 40x30 one at full size.
|
||||||
|
*/
|
||||||
|
class tst_svgsmallpictures : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
QString m_fixture;
|
||||||
|
|
||||||
|
/// Pixel size of each picture embedded in @p svg, smallest first
|
||||||
|
static QList<QSize> embeddedSizes(const QByteArray &svg)
|
||||||
|
{
|
||||||
|
static const QRegularExpression data(
|
||||||
|
QStringLiteral("data:image/png;base64,([A-Za-z0-9+/=\\s]+)"));
|
||||||
|
QList<QSize> sizes;
|
||||||
|
auto it = data.globalMatch(QString::fromLatin1(svg));
|
||||||
|
while (it.hasNext())
|
||||||
|
sizes << QImage::fromData(QByteArray::fromBase64(
|
||||||
|
it.next().captured(1).toLatin1())).size();
|
||||||
|
std::sort(sizes.begin(), sizes.end(), [](const QSize &a, const QSize &b) {
|
||||||
|
return a.width() < b.width();
|
||||||
|
});
|
||||||
|
return sizes;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray exportWithCli(const QStringList &extra)
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||||
|
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||||
|
QProcess proc;
|
||||||
|
proc.setProcessEnvironment(env);
|
||||||
|
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||||
|
QStringList{QStringLiteral("--export-svg"), m_fixture, dir.path()} + extra);
|
||||||
|
if (!proc.waitForFinished(60000) || proc.exitCode() != 0)
|
||||||
|
return {};
|
||||||
|
const QStringList files = QDir(dir.path()).entryList({QStringLiteral("*.svg")});
|
||||||
|
QFile svg(QDir(dir.path()).filePath(files.value(0)));
|
||||||
|
return svg.open(QIODevice::ReadOnly) ? svg.readAll() : QByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// As ExportDialog::generateSvg(): the folio drawn into a QPicture,
|
||||||
|
/// played into the SVG generator
|
||||||
|
static QByteArray exportLikeTheDialog(Diagram *diagram, bool small)
|
||||||
|
{
|
||||||
|
const QSize size = diagram->imageSize();
|
||||||
|
QPicture picture;
|
||||||
|
diagram->setSmallPictures(small);
|
||||||
|
diagram->toPaintDevice(picture, size.width(), size.height());
|
||||||
|
diagram->setSmallPictures(false);
|
||||||
|
|
||||||
|
QByteArray svg;
|
||||||
|
QBuffer buffer(&svg);
|
||||||
|
buffer.open(QIODevice::WriteOnly);
|
||||||
|
QSvgGenerator generator;
|
||||||
|
generator.setSize(size);
|
||||||
|
generator.setOutputDevice(&buffer);
|
||||||
|
QPainter painter(&generator);
|
||||||
|
picture.play(&painter);
|
||||||
|
painter.end();
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void initTestCase()
|
||||||
|
{
|
||||||
|
m_fixture = QFINDTESTDATA("fixtures/svg_small_pictures.qet");
|
||||||
|
QVERIFY2(!m_fixture.isEmpty(), "fixture project not found");
|
||||||
|
QVERIFY(QetAppTest::editor());
|
||||||
|
}
|
||||||
|
|
||||||
|
void cliFullResolutionByDefault()
|
||||||
|
{
|
||||||
|
const QByteArray svg = exportWithCli({});
|
||||||
|
QVERIFY2(!svg.isEmpty(), "--export-svg failed");
|
||||||
|
QCOMPARE(embeddedSizes(svg), (QList<QSize>{QSize(40, 30), QSize(800, 600)}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void cliSmallPictures()
|
||||||
|
{
|
||||||
|
const QByteArray svg = exportWithCli({QStringLiteral("--svg-small-pictures")});
|
||||||
|
QVERIFY2(!svg.isEmpty(), "--export-svg --svg-small-pictures failed");
|
||||||
|
// 800 x 600 drawn at 0.1, times 4; the 40 x 30 one is kept whole
|
||||||
|
QCOMPARE(embeddedSizes(svg), (QList<QSize>{QSize(40, 30), QSize(320, 240)}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void dialogSmallPicturesThenFullAgain()
|
||||||
|
{
|
||||||
|
QFile file(m_fixture);
|
||||||
|
QVERIFY(file.open(QIODevice::ReadOnly));
|
||||||
|
QDomDocument document;
|
||||||
|
QVERIFY(document.setContent(&file));
|
||||||
|
const QDomElement image_xml = document.elementsByTagName(
|
||||||
|
QStringLiteral("image")).at(0).toElement();
|
||||||
|
Diagram *diagram = QetAppTest::diagram();
|
||||||
|
auto *picture = new DiagramImageItem();
|
||||||
|
QVERIFY(picture->fromXml(image_xml));
|
||||||
|
diagram->addItem(picture);
|
||||||
|
|
||||||
|
QCOMPARE(embeddedSizes(exportLikeTheDialog(diagram, true)),
|
||||||
|
QList<QSize>{QSize(320, 240)});
|
||||||
|
// The option lasts for one export only
|
||||||
|
QVERIFY(!diagram->smallPictures());
|
||||||
|
QCOMPARE(embeddedSizes(exportLikeTheDialog(diagram, false)),
|
||||||
|
QList<QSize>{QSize(800, 600)});
|
||||||
|
delete picture;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QET_APP_TEST_MAIN(tst_svgsmallpictures)
|
||||||
|
#include "tst_svgsmallpictures.moc"
|
||||||
Reference in New Issue
Block a user