mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-20 05:14:16 +02:00
Merge remote-tracking branch 'upstream/master' into fix/keep-unloadable-items
This commit is contained in:
+1599
-1026
File diff suppressed because it is too large
Load Diff
+1603
-1012
File diff suppressed because it is too large
Load Diff
+1600
-1024
File diff suppressed because it is too large
Load Diff
+1596
-1024
File diff suppressed because it is too large
Load Diff
+1561
-1025
File diff suppressed because it is too large
Load Diff
+1596
-1024
File diff suppressed because it is too large
Load Diff
+1603
-1012
File diff suppressed because it is too large
Load Diff
+1603
-1012
File diff suppressed because it is too large
Load Diff
+1562
-1019
File diff suppressed because it is too large
Load Diff
+1572
-1021
File diff suppressed because it is too large
Load Diff
+1570
-1019
File diff suppressed because it is too large
Load Diff
+1585
-1017
File diff suppressed because it is too large
Load Diff
+1604
-1013
File diff suppressed because it is too large
Load Diff
+1599
-1012
File diff suppressed because it is too large
Load Diff
+1570
-1023
File diff suppressed because it is too large
Load Diff
+1580
-1018
File diff suppressed because it is too large
Load Diff
+1589
-1017
File diff suppressed because it is too large
Load Diff
+1596
-1024
File diff suppressed because it is too large
Load Diff
+1596
-1024
File diff suppressed because it is too large
Load Diff
+1562
-1019
File diff suppressed because it is too large
Load Diff
+1604
-1024
File diff suppressed because it is too large
Load Diff
+1621
-1030
File diff suppressed because it is too large
Load Diff
+1593
-1017
File diff suppressed because it is too large
Load Diff
+1577
-1026
File diff suppressed because it is too large
Load Diff
+1565
-1018
File diff suppressed because it is too large
Load Diff
+1607
-1012
File diff suppressed because it is too large
Load Diff
+1678
-1059
File diff suppressed because it is too large
Load Diff
+1569
-1018
File diff suppressed because it is too large
Load Diff
+1565
-1018
File diff suppressed because it is too large
Load Diff
+1589
-1017
File diff suppressed because it is too large
Load Diff
+1585
-1017
File diff suppressed because it is too large
Load Diff
+1607
-1012
File diff suppressed because it is too large
Load Diff
+1599
-1012
File diff suppressed because it is too large
Load Diff
+1599
-1012
File diff suppressed because it is too large
Load Diff
+44
-8
@@ -1072,6 +1072,23 @@ def _user_collections() -> dict:
|
||||
return found
|
||||
|
||||
|
||||
def _keep_tail(text: str, tail: int) -> str:
|
||||
"""The last `tail` characters of a run's output, but every line an edit
|
||||
script reported through its marker kept whole, wherever it stands.
|
||||
|
||||
A large qet_edit logs far more than `tail` characters; cut from the
|
||||
front, it lost the capability report the script makes first, and the
|
||||
tool then said the binary had never run the script."""
|
||||
if len(text) <= tail:
|
||||
return text
|
||||
marked = [ln for ln in text.splitlines() if "QETEDIT " in ln]
|
||||
rest = text[-tail:]
|
||||
if not marked:
|
||||
return rest
|
||||
kept = set(rest.splitlines())
|
||||
return "\n".join([ln for ln in marked if ln not in kept] + [rest])
|
||||
|
||||
|
||||
def _run_qet(binary: str, args: list[str], timeout: int = 180,
|
||||
elements_dir: str | None = None,
|
||||
script: str | None = None, tail: int = 4000,
|
||||
@@ -1150,7 +1167,7 @@ def _run_qet(binary: str, args: list[str], timeout: int = 180,
|
||||
"hint": "a modal dialog during load will hang a headless "
|
||||
"run; check the project's format version"}
|
||||
result = {"ok": p.returncode == 0, "exit_code": p.returncode,
|
||||
"stdout": p.stdout[-tail:], "stderr": p.stderr[-tail:]}
|
||||
"stdout": _keep_tail(p.stdout, tail), "stderr": _keep_tail(p.stderr, tail)}
|
||||
# QElectroTech refuses --run when scripting is switched off, which
|
||||
# it is by default. Its own message is clear but French, and it
|
||||
# names a settings dialog that nobody driving this server is
|
||||
@@ -1581,6 +1598,13 @@ OPS = {
|
||||
"remove_folio": ("removeFolio", [("folio", "folio")]),
|
||||
"set_folio": ("setFolioProperty", [("folio", "folio"), ("property", "str"),
|
||||
("value", "str")]),
|
||||
# A cable drawn across conductors, as the cable tool draws it: every
|
||||
# conductor the line crosses gets the next core and its colour.
|
||||
"add_cable": ("addCable", [("folio", "folio"), ("designation", "str"),
|
||||
("type", "str"), ("core_colors", "list"),
|
||||
("length", "str"),
|
||||
("x1", "num"), ("y1", "num"),
|
||||
("x2", "num"), ("y2", "num")]),
|
||||
"add_terminal_strip": ("addTerminalStrip", [("installation", "str"), ("location", "str"),
|
||||
("name", "str")]),
|
||||
"remove_terminal_strip": ("removeTerminalStrip", [("strip", "folio")]),
|
||||
@@ -2772,22 +2796,34 @@ def tool_element_search(directory: str, query: str = "", link_type: str | None =
|
||||
# either would bury the real findings.
|
||||
# - "simple" elements are checked for duplicates too but only as a warning:
|
||||
# industrial.qet reuses V1..V6 across folios on purpose.
|
||||
#
|
||||
# A label names one device per place: the same "-F1" in two distribution
|
||||
# boards (+UV1-F1, +UV2-F1) is two devices, as in IEC 81346 / EPLAN. The
|
||||
# place is the element's own plant/location, else its folio's -- what the
|
||||
# label formulas %M/%LM show. element_nomenclature_view carries only the
|
||||
# element's, so these two checks read the tables.
|
||||
_LABELS_PER_PLACE_SQL = (
|
||||
"SELECT ei.label AS label, "
|
||||
"COALESCE(NULLIF(ei.plant,''), di.plant, '') AS plant, "
|
||||
"COALESCE(NULLIF(ei.location,''), di.locmach, '') AS location, COUNT(*) AS n "
|
||||
"FROM element_info ei JOIN element e ON ei.element_uuid = e.uuid "
|
||||
"JOIN diagram_info di ON di.diagram_uuid = e.diagram_uuid "
|
||||
"WHERE COALESCE(ei.label,'') <> '' AND e.type = '{type}' "
|
||||
"AND COALESCE(LOWER(TRIM(ei.exclude_from_bom)), '') NOT IN ('true', '1', 'yes', 'on') "
|
||||
"GROUP BY ei.label, 2, 3 HAVING n > 1 ORDER BY n DESC, ei.label")
|
||||
|
||||
CHECKS = {
|
||||
"duplicate_master_labels": {
|
||||
"severity": "error",
|
||||
"note": "Two master elements with the same label are ambiguous in every "
|
||||
"report that keys on it (BOM, cross-references, wiring list).",
|
||||
"sql": "SELECT label, COUNT(*) AS n FROM element_nomenclature_view "
|
||||
"WHERE COALESCE(label,'') <> '' AND element_type = 'master' "
|
||||
"GROUP BY label HAVING n > 1 ORDER BY n DESC, label",
|
||||
"sql": _LABELS_PER_PLACE_SQL.format(type="master"),
|
||||
},
|
||||
"duplicate_simple_labels": {
|
||||
"severity": "warning",
|
||||
"note": "Legitimate when a label is reused on purpose across folios "
|
||||
"(industrial.qet does); worth a look otherwise.",
|
||||
"sql": "SELECT label, COUNT(*) AS n FROM element_nomenclature_view "
|
||||
"WHERE COALESCE(label,'') <> '' AND element_type = 'simple' "
|
||||
"GROUP BY label HAVING n > 1 ORDER BY n DESC, label",
|
||||
"sql": _LABELS_PER_PLACE_SQL.format(type="simple"),
|
||||
},
|
||||
"unlabelled_masters": {
|
||||
"severity": "warning",
|
||||
@@ -5142,7 +5178,7 @@ TOOLS = [
|
||||
"it to one element AFTER it is placed (add_element does not number "
|
||||
"what it places); slaves and reports are refused, since they take "
|
||||
"their label from their master. "
|
||||
"Terminal strips: add_terminal_strip returns an index (name "
|
||||
"Cables: add_cable (folio, designation -- empty for the next free name --, type, core_colors, length, x1, y1, x2, y2) draws a new cable's trunk line across the conductors between the two points, as the cable tool does: each crossed conductor, in order along the line, gets the next core and the colour core_colors lists at that place; a conductor another cable already holds makes the op fail. Returns the cable's uuid. Terminal strips: add_terminal_strip returns an index (name "
|
||||
"it \"$id\"); add_to_strip puts a terminal-type element on "
|
||||
"it, and refuses any other kind. group_terminals/bridge_terminals "
|
||||
"take \"indices\" (at least two) into that strip's real-terminal "
|
||||
|
||||
@@ -437,6 +437,9 @@ class EditValidation(unittest.TestCase):
|
||||
"set_element_text": el + [{"op": "set_element_text", "folio": "$f", "element": "$e", "index": 0,
|
||||
"property": "x", "value": "5"}],
|
||||
"delete_element_text": el + [{"op": "delete_element_text", "folio": "$f", "element": "$e", "index": 0}],
|
||||
"add_cable": [f, {"op": "add_cable", "id": "k", "folio": "$f", "designation": "-W1",
|
||||
"type": "NYM-J 3x1,5", "core_colors": ["BN", "BU", "GNYE"],
|
||||
"length": "2 m", "x1": 0, "y1": 0, "x2": 100, "y2": 0}],
|
||||
"add_terminal_strip": [{"op": "add_terminal_strip", "installation": "a", "location": "b", "name": "c"}],
|
||||
"remove_terminal_strip": [{"op": "remove_terminal_strip", "strip": 0}],
|
||||
"add_to_strip": el + [{"op": "add_to_strip", "strip": 0, "folio": "$f", "element": "$e"}],
|
||||
@@ -4534,12 +4537,28 @@ class Integration(unittest.TestCase):
|
||||
for i in range(4)],
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$e0", "from_terminal": 0, "to": "$e1", "to_terminal": 0},
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$e2", "from_terminal": 0, "to": "$e3", "to_terminal": 0},
|
||||
{"op": "set_conductor", "folio": "$f", "element": "$e2", "terminal": 0, "property": "num", "value": "W2"},
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$e0", "from_terminal": 0, "to": "$e2", "to_terminal": 0},
|
||||
], self.sb.p("conflict.qet"), elements_dir=ELEMENTS, timeout=60)
|
||||
self.assertFalse(r.get("timed_out"), "the call hung on a modal dialog")
|
||||
self.assertFalse(r["ok"])
|
||||
self.assertEqual(r["operations"][-1]["result"], False)
|
||||
|
||||
def test_joining_two_agreeing_potentials_asks_nothing(self):
|
||||
"""Two potentials with the same properties leave nothing to choose:
|
||||
the wire joining them is drawn, headless as well."""
|
||||
base = self.sb.new()
|
||||
r = self.ok(self.sb.edit(base, [
|
||||
{"op": "add_folio", "id": "f"},
|
||||
*[{"op": "add_element", "id": f"e{i}", "folio": "$f", "path": COIL, "x": 100 + i * 200, "y": 100}
|
||||
for i in range(4)],
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$e0", "from_terminal": 0, "to": "$e1", "to_terminal": 0},
|
||||
{"op": "set_conductor", "folio": "$f", "element": "$e0", "terminal": 0, "property": "num", "value": "N"},
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$e2", "from_terminal": 0, "to": "$e3", "to_terminal": 0},
|
||||
{"op": "set_conductor", "folio": "$f", "element": "$e2", "terminal": 0, "property": "num", "value": "N"},
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$e0", "from_terminal": 0, "to": "$e2", "to_terminal": 0}]))
|
||||
self.assertEqual([c["num"] for c in m.tool_conductors(r["output"])["conductors"]], ["N"] * 3)
|
||||
|
||||
# ---- edit: conductor properties, potentials ----
|
||||
|
||||
def test_conductor_property_applies_to_the_whole_potential(self):
|
||||
@@ -4555,6 +4574,34 @@ class Integration(unittest.TestCase):
|
||||
rows = m.tool_conductors(r["output"])["conductors"]
|
||||
self.assertEqual([c["num"] for c in rows], ["W7", "W7"])
|
||||
|
||||
def test_new_conductor_inherits_a_potential_through_terminal_and_report(self):
|
||||
"""A wire drawn to the free side of a terminal element, or to a
|
||||
report linked to a wired one, is on that potential already: it
|
||||
takes the potential's number and line style, not the folio's
|
||||
defaults (ConductorCreator::existingPotential)."""
|
||||
base = self.sb.new()
|
||||
ops = [{"op": "add_folio", "id": "f"}, {"op": "add_folio", "id": "g"},
|
||||
{"op": "add_element", "id": "a", "folio": "$f", "path": COIL, "x": 100, "y": 100},
|
||||
{"op": "add_element", "id": "t", "folio": "$f", "path": TERMINAL, "x": 300, "y": 300},
|
||||
{"op": "add_element", "id": "b", "folio": "$f", "path": COIL, "x": 500, "y": 100},
|
||||
{"op": "add_element", "id": "go", "folio": "$f", "path": NEXT_REPORT, "x": 700, "y": 100},
|
||||
{"op": "add_element", "id": "in", "folio": "$g", "path": PREVIOUS_REPORT, "x": 100, "y": 100},
|
||||
{"op": "add_element", "id": "c", "folio": "$g", "path": COIL, "x": 300, "y": 100},
|
||||
{"op": "link_elements", "folio": "$f", "element": "$go", "to_folio": "$g", "to": "$in"},
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$a", "from_terminal": 0, "to": "$t", "to_terminal": 0},
|
||||
{"op": "set_conductor", "folio": "$f", "element": "$a", "terminal": 0, "property": "num", "value": "W7"},
|
||||
{"op": "set_conductor", "folio": "$f", "element": "$a", "terminal": 0, "property": "style", "value": "dashed"},
|
||||
# the terminal's other side, then on through the report
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$t", "from_terminal": 1, "to": "$b", "to_terminal": 0},
|
||||
{"op": "add_conductor", "folio": "$f", "from": "$b", "from_terminal": 0, "to": "$go", "to_terminal": 0},
|
||||
{"op": "add_conductor", "folio": "$g", "from": "$in", "from_terminal": 0, "to": "$c", "to_terminal": 0}]
|
||||
r = self.ok(self.sb.edit(base, ops))
|
||||
rows = m.tool_conductors(r["output"])["conductors"]
|
||||
self.assertEqual([c["num"] for c in rows], ["W7"] * 4)
|
||||
styles = {c["value"] for c in m.tool_conductors(r["output"], attribute="style")["conductors"]}
|
||||
self.assertEqual(len(styles), 1, styles)
|
||||
self.assertIn("dash", styles.pop())
|
||||
|
||||
# ---- conductor manual routing ----
|
||||
|
||||
def test_move_conductor_segment_reroutes_the_path(self):
|
||||
@@ -5828,6 +5875,22 @@ class Integration(unittest.TestCase):
|
||||
line = [l for l in (out["stdout"] + out["stderr"]).splitlines() if l.startswith("DB ")][0]
|
||||
self.assertEqual(json.loads(line[3:]), [{"wire_number": "W1"}])
|
||||
|
||||
def test_duplicate_labels_are_counted_per_location(self):
|
||||
"""-F1 in two distribution boards is two devices: the folio's
|
||||
location (+UV1, +UV2) is part of the name, as %LM shows it. KM1 on
|
||||
+UV1 and +UV2 is fine, a second KM1 on +UV2 is the finding."""
|
||||
base = self.sb.new(folios=3)
|
||||
ops = []
|
||||
for folio, ort in enumerate(("UV1", "UV2", "UV2")):
|
||||
ops += [{"op": "set_folio", "folio": folio, "property": "locmach", "value": ort},
|
||||
{"op": "add_element", "id": f"k{folio}", "folio": folio, "path": COIL,
|
||||
"x": 100, "y": 100},
|
||||
{"op": "set_label", "folio": folio, "element": f"$k{folio}", "label": "KM1"}]
|
||||
r = self.ok(self.sb.edit(base, ops))
|
||||
found = {f["check"]: f for f in m.tool_check(BINARY, r["output"])["findings"]}
|
||||
self.assertEqual(found["duplicate_master_labels"]["rows"],
|
||||
[{"label": "KM1", "plant": "", "location": "UV2", "n": 2}])
|
||||
|
||||
def test_check_fires_on_a_project_with_known_defects(self):
|
||||
"""Newly placed elements store NULL where a loaded file stores '', and
|
||||
`col = ''` matches only the second: the first version of these checks
|
||||
@@ -6626,6 +6689,23 @@ class ZippedProjects(unittest.TestCase):
|
||||
self.assertNotEqual(*rotated[0]["orientation"])
|
||||
|
||||
|
||||
class TestKeepTail(unittest.TestCase):
|
||||
"""A large edit logs more than the kept tail; the script's own report
|
||||
lines must survive the cut, or qet_edit says the script never ran."""
|
||||
|
||||
def test_short_output_is_untouched(self):
|
||||
self.assertEqual(m._keep_tail("a\nb", 100), "a\nb")
|
||||
|
||||
def test_marker_lines_survive_the_cut(self):
|
||||
first = 'QETEDIT {"kind": "capabilities", "missing": []}'
|
||||
text = first + "\n" + "x" * 1000 + "\n" + 'QETEDIT {"kind": "save", "result": true}'
|
||||
kept = m._keep_tail(text, 100)
|
||||
self.assertIn(first, kept)
|
||||
self.assertIn('"kind": "save"', kept)
|
||||
self.assertEqual(kept.count('"kind": "save"'), 1)
|
||||
self.assertEqual(m._parse_script_output(kept).get("missing_methods"), [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"binary : {BINARY or '(none: integration tests skipped)'}")
|
||||
print(f"elements : {ELEMENTS or '(none)'}")
|
||||
|
||||
@@ -2615,6 +2615,8 @@ void Diagram::folioSequentialsFromXml(const QDomElement &root,
|
||||
- linking the elements waiting to be linked
|
||||
- Refresh the connection of the dynamic element text item
|
||||
(use for text with source of text label)
|
||||
- Fill the texts of a report (or a wire label) that show the
|
||||
properties of the wire they are on.
|
||||
*/
|
||||
void Diagram::refreshContents()
|
||||
{
|
||||
@@ -2624,6 +2626,25 @@ void Diagram::refreshContents()
|
||||
elmt->initLink(project());
|
||||
for (auto &deti : elmt->dynamicTextItems())
|
||||
deti->refreshLabelConnection();
|
||||
/*
|
||||
* A report's texts showing a wire's properties are filled when
|
||||
* the wires are added, which is after the symbols are read
|
||||
* (Diagram::buildContents()), and once more 100 ms later by
|
||||
* DynamicElementTextItem::conductorWasAdded(). Both run after
|
||||
* the project left the parsing state, so the alignment is live
|
||||
* and the fill would take the rect of the still empty text for
|
||||
* anchor -- a right aligned one then shifts by the whole width
|
||||
* of its text, on every open.
|
||||
*
|
||||
* A linked report gets that fill here already, through
|
||||
* initLink() -> linkedElementChanged() -> reportChanged(), while
|
||||
* the project is still parsing; buildFolios() relies on it. An
|
||||
* unlinked one, and one whose partner is on a folio not built
|
||||
* yet, got nothing: fill them here too, in that same state. The
|
||||
* later fill then finds the text already set and changes
|
||||
* nothing.
|
||||
*/
|
||||
elmt->updateConductorTexts();
|
||||
}
|
||||
|
||||
for (auto &conductor : dc_.conductors()) {
|
||||
|
||||
@@ -156,9 +156,11 @@ QTreeWidgetItem *ElementsPanel::addProject(QETProject *project,
|
||||
|
||||
if (first_add){
|
||||
qtwi_project -> setExpanded(true);
|
||||
// on adding an project select first diagram
|
||||
setCurrentItem(qtwi_project -> child(0));
|
||||
qtwi_project -> child(0)->setSelected(true);
|
||||
// on adding a project select first diagram, once the project item is attached
|
||||
if (qtwi_project -> treeWidget()) {
|
||||
setCurrentItem(qtwi_project -> child(0));
|
||||
qtwi_project -> child(0)->setSelected(true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 2. Check whether we can restore the previous selection
|
||||
|
||||
@@ -2726,22 +2726,32 @@ bool Element::definitionGeometryMatches(const QDomElement &definition) const
|
||||
|
||||
/**
|
||||
* @brief Element::updateConductorTexts
|
||||
*Slot that is triggered when a cable is *
|
||||
*connected to or disconnected from a terminal on this component.
|
||||
*/
|
||||
/**
|
||||
* @brief Element::updateConductorTexts
|
||||
* Refresh the texts of this symbol that show the properties of the wire
|
||||
* it is on: a wire label's own, and a folio report's (its function,
|
||||
* section...). Slot triggered when a wire is connected to or
|
||||
* disconnected from a terminal of this symbol.
|
||||
*/
|
||||
void Element::updateConductorTexts()
|
||||
{
|
||||
if (m_link_type != Element::ConductorDefinition) {
|
||||
return;
|
||||
if (m_link_type == Element::ConductorDefinition)
|
||||
{
|
||||
for (DynamicElementTextItem *deti : m_dynamic_text_list) {
|
||||
if (deti) {
|
||||
deti->setPotentialConductor();
|
||||
deti->updateLabel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (DynamicElementTextItem *deti : m_dynamic_text_list) {
|
||||
if (deti) {
|
||||
deti->setPotentialConductor();
|
||||
deti->updateLabel();
|
||||
else if (m_link_type & Element::AllReport)
|
||||
{
|
||||
//setPotentialConductor() ends on conductorPropertiesChanged(),
|
||||
//which fills those texts. No updateLabel() here: a report's
|
||||
//"label" text is not its own information but the linked
|
||||
//folio's, refreshed by updateReportText().
|
||||
for (DynamicElementTextItem *deti : m_dynamic_text_list) {
|
||||
if (deti) {
|
||||
deti->setPotentialConductor();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,10 @@
|
||||
#include <QDir>
|
||||
#include <QDomDocument>
|
||||
#include <QFileInfo>
|
||||
#include "../cable/addcablecommand.h"
|
||||
#include "../cable/cable.h"
|
||||
#include "../cable/cablemanager.h"
|
||||
#include "../cable/cablepart.h"
|
||||
#include <QFont>
|
||||
#include <QImage>
|
||||
#include <QPixmap>
|
||||
@@ -2262,6 +2266,108 @@ int QetScriptApi::addShape(int folioIndex, const QString &type,
|
||||
return sortedShapes(folioIndex).indexOf(shape);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetScriptApi::addCable
|
||||
Draw a cable across conductors, as the cable tool does: the line from
|
||||
(x1, y1) to (x2, y2) on the folio becomes the cable's trunk, every
|
||||
conductor it crosses gets the next core, in order along the line, and
|
||||
with it the colour @p coreColors lists at that place. The cable is a
|
||||
new one, named @p designation (the next free name when empty), of
|
||||
type @p type, as long as @p length says. One undo step, through the
|
||||
same AddCableCommand the tool pushes.
|
||||
A conductor another cable already holds is not taken away from it:
|
||||
the call is refused instead, since a script cannot be asked, as the
|
||||
tool's dialog asks, whether to move it.
|
||||
@return the cable's uuid, or an empty string
|
||||
*/
|
||||
QString QetScriptApi::addCable(int folioIndex, const QString &designation,
|
||||
const QString &type, const QStringList &coreColors,
|
||||
const QString &length,
|
||||
double x1, double y1, double x2, double y2)
|
||||
{
|
||||
if (!m_project) return QString();
|
||||
if (m_project->isReadOnly()) {
|
||||
log(QStringLiteral("qet.addCable: project is read-only"));
|
||||
return QString();
|
||||
}
|
||||
const QList<Diagram *> diagrams = m_project->folios();
|
||||
if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString();
|
||||
Diagram *diagram = built(diagrams.at(folioIndex));
|
||||
|
||||
const QLineF line(QPointF(x1, y1), QPointF(x2, y2));
|
||||
const QList<CableCrossing> crossings = CableManager::crossedConductors(diagram, line);
|
||||
if (crossings.isEmpty()) {
|
||||
log(QStringLiteral("qet.addCable: the line crosses no conductor"));
|
||||
return QString();
|
||||
}
|
||||
for (const CableCrossing &crossing : crossings) {
|
||||
if (crossing.conductor && !crossing.conductor->properties().m_cable_uuid.isNull()) {
|
||||
log(QStringLiteral("qet.addCable: a crossed conductor already belongs to a cable"));
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
Cable *cable = m_project->newCable();
|
||||
cable->setType(type);
|
||||
cable->setCoreCount(qMax(int(coreColors.size()), int(crossings.size())));
|
||||
cable->setCoreColors(coreColors);
|
||||
cable->setLength(length);
|
||||
if (designation.trimmed().isEmpty()) {
|
||||
cable->setDesignation(CableManager::nextDesignation(m_project, diagram));
|
||||
} else {
|
||||
cable->setDesignation(designation.trimmed());
|
||||
cable->setDesignationByHand(true);
|
||||
}
|
||||
|
||||
CablePartData part;
|
||||
part.uuid = QUuid::createUuid();
|
||||
part.diagram = diagram->uuid();
|
||||
part.p1 = line.p1();
|
||||
part.p2 = line.p2();
|
||||
cable->addPart(part);
|
||||
|
||||
int core_number = 0;
|
||||
for (const CableCrossing &crossing : crossings)
|
||||
{
|
||||
Conductor *conductor = crossing.conductor;
|
||||
if (!conductor) continue;
|
||||
CableManager::claimConductor(m_project, conductor, cable);
|
||||
//A cable binds a conductor by its identity (@see
|
||||
//CableCreateDialog::assignCrossings)
|
||||
conductor->setUuid(conductor->uuid());
|
||||
CableCore core;
|
||||
core.core = core_number++;
|
||||
core.part = part.uuid;
|
||||
core.conductor = conductor->uuid();
|
||||
core.position = crossing.position;
|
||||
cable->setCore(core);
|
||||
}
|
||||
|
||||
auto *item = new CablePart(part);
|
||||
item->setCable(cable);
|
||||
diagram->addItem(item);
|
||||
m_project->undoStack()->push(new AddCableCommand(cable, item, diagram, true));
|
||||
return cable->uuid().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetScriptApi::cables
|
||||
@return one line per cable of the project: designation, type, cores
|
||||
used of cores, length and uuid
|
||||
*/
|
||||
QStringList QetScriptApi::cables() const
|
||||
{
|
||||
QStringList out;
|
||||
if (!m_project) return out;
|
||||
for (Cable *cable : m_project->cables()) {
|
||||
out << QStringLiteral("%1: %2, %3/%4 cores, %5 %6")
|
||||
.arg(cable->designation(), cable->type())
|
||||
.arg(cable->usedCoreCount()).arg(cable->coreCount())
|
||||
.arg(cable->length(), cable->uuid().toString());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
namespace {
|
||||
const QStringList &shapePropertyNames()
|
||||
{
|
||||
|
||||
@@ -490,6 +490,13 @@ class QetScriptApi : public QObject
|
||||
Q_INVOKABLE bool setTextRotation(int folioIndex, int textIndex, double angle);
|
||||
Q_INVOKABLE bool deleteText(int folioIndex, int textIndex);
|
||||
|
||||
// -- cables --
|
||||
Q_INVOKABLE QString addCable(int folioIndex, const QString &designation,
|
||||
const QString &type, const QStringList &coreColors,
|
||||
const QString &length,
|
||||
double x1, double y1, double x2, double y2);
|
||||
Q_INVOKABLE QStringList cables() const;
|
||||
|
||||
Q_INVOKABLE QStringList shapes(int folioIndex) const;
|
||||
Q_INVOKABLE int shapeIndex(int folioIndex, const QString &uuid) const;
|
||||
Q_INVOKABLE int addShape(int folioIndex, const QString &type,
|
||||
|
||||
@@ -128,7 +128,50 @@ bool ConductorCreator::needsPotentialChoice(const QList<Terminal *> &terminals_l
|
||||
if (terminals_list.size() <= 1) {
|
||||
return false;
|
||||
}
|
||||
return existingPotential(terminals_list).size() >= 2;
|
||||
return distinctProperties(existingPotential(terminals_list)).size() >= 2;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorCreator::inheritedProperties
|
||||
The properties a new wire takes from @p conductor's potential. A
|
||||
cable's core names the cable and the core in m_cable, m_cable_uuid
|
||||
and m_cable_slot (written by CableManager): none of that belongs to a
|
||||
new wire, which is in no cable yet. The text is the potential's wire
|
||||
number and is kept, unless it only repeats the core label.
|
||||
@param conductor : a conductor of the potential
|
||||
@return the properties for a new conductor of that potential
|
||||
*/
|
||||
ConductorProperties ConductorCreator::inheritedProperties(const Conductor *conductor)
|
||||
{
|
||||
ConductorProperties properties = conductor->properties();
|
||||
if (!properties.m_cable_uuid.isNull())
|
||||
{
|
||||
if (properties.text == properties.m_cable)
|
||||
properties.text.clear();
|
||||
properties.m_cable.clear();
|
||||
properties.m_cable_uuid = QUuid();
|
||||
properties.m_cable_slot = -1;
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorCreator::distinctProperties
|
||||
@param potentials : one conductor of each potential (existingPotential())
|
||||
@return the properties a new wire could inherit from them, each once:
|
||||
two potentials that agree leave nothing to choose
|
||||
*/
|
||||
QList<ConductorProperties> ConductorCreator::distinctProperties(const QList<Conductor *> &potentials)
|
||||
{
|
||||
QList<ConductorProperties> list;
|
||||
for (const Conductor *c : potentials)
|
||||
{
|
||||
const ConductorProperties properties = inheritedProperties(c);
|
||||
if (!list.contains(properties)) {
|
||||
list.append(properties);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,28 +189,24 @@ bool ConductorCreator::setUpPropertieToUse()
|
||||
//we get one of them
|
||||
if (!potentials.isEmpty())
|
||||
{
|
||||
if (potentials.size() >= 2)
|
||||
const QList<ConductorProperties> cp_list = distinctProperties(potentials);
|
||||
if (cp_list.size() >= 2)
|
||||
{
|
||||
QList <ConductorProperties> cp_list;
|
||||
for(Conductor *c : potentials) {
|
||||
cp_list.append(c->properties());
|
||||
}
|
||||
|
||||
bool cancelled = false;
|
||||
m_properties = PotentialSelectorDialog::chosenProperties(cp_list, nullptr, &cancelled);
|
||||
if (cancelled) {
|
||||
return false;
|
||||
}
|
||||
for (Conductor *c : potentials) {
|
||||
if (c->properties() == m_properties) {
|
||||
m_sequential_number = c->sequenceNum();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (potentials.size() == 1)
|
||||
else
|
||||
{
|
||||
m_properties = potentials.first()->properties();
|
||||
m_sequential_number = potentials.first()->sequenceNum();
|
||||
m_properties = cp_list.first();
|
||||
}
|
||||
for (Conductor *c : potentials) {
|
||||
if (inheritedProperties(c) == m_properties) {
|
||||
m_sequential_number = c->sequenceNum();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -179,46 +218,65 @@ bool ConductorCreator::setUpPropertieToUse()
|
||||
|
||||
/**
|
||||
@brief ConductorCreator::existingPotential
|
||||
Return the list of existing potential of
|
||||
the terminal list
|
||||
Return one conductor of each existing potential the terminals of
|
||||
@p terminals_list are on.
|
||||
|
||||
A terminal without a wire of its own can still be on a potential: the
|
||||
other side of a terminal element, a folio report linked to one that is
|
||||
wired. Such a terminal is followed through relatedPotentialTerminal()
|
||||
until a wired terminal is found -- the rule Conductor::
|
||||
relatedPotentialConductors() applies, so that a wire drawn to the
|
||||
free side of a terminal takes the properties (number, colour, line
|
||||
style...) of the potential the terminal already carries instead of the
|
||||
folio's defaults.
|
||||
Two terminals on the same potential give that potential once.
|
||||
@param terminals_list the terminals to inspect
|
||||
@return c_list QList<Conductor *>
|
||||
*/
|
||||
QList<Conductor *> ConductorCreator::existingPotential(const QList<Terminal *> &terminals_list)
|
||||
{
|
||||
QList<Conductor *> c_list;
|
||||
QList<Terminal *> t_exclude;
|
||||
|
||||
QList<QSet<Conductor *>> potentials;
|
||||
|
||||
for (Terminal *t : terminals_list)
|
||||
{
|
||||
if (t_exclude.contains(t)) {
|
||||
Conductor *seed = nullptr;
|
||||
QList<Terminal *> to_visit {t};
|
||||
QSet<const Terminal *> visited;
|
||||
while (!to_visit.isEmpty() && !seed)
|
||||
{
|
||||
Terminal *current = to_visit.takeFirst();
|
||||
if (visited.contains(current)) {
|
||||
continue;
|
||||
}
|
||||
visited.insert(current);
|
||||
if (!current->conductors().isEmpty()) {
|
||||
seed = current->conductors().first();
|
||||
} else {
|
||||
to_visit += relatedPotentialTerminal(current, true);
|
||||
}
|
||||
}
|
||||
if (!seed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!t->conductors().isEmpty())
|
||||
{
|
||||
c_list.append(t->conductors().first());
|
||||
|
||||
//We must check if m_terminals_list contains a terminal
|
||||
//in the same potential of c, and if true, exclude this terminal from the search.
|
||||
for (Conductor *c : t->conductors().first()->relatedPotentialConductors(false))
|
||||
{
|
||||
if (terminals_list.contains(c->terminal1)) {
|
||||
t_exclude.append(c->terminal1);
|
||||
} else if (terminals_list.contains(c->terminal2)) {
|
||||
t_exclude.append(c->terminal2);
|
||||
}
|
||||
|
||||
bool known = false;
|
||||
for (const QSet<Conductor *> &potential : std::as_const(potentials)) {
|
||||
if (potential.contains(seed)) {
|
||||
known = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (t->parentElement()->linkType() & Element::AllReport && !t->parentElement()->isFree())
|
||||
{
|
||||
Element *linked_report = t->parentElement()->linkedElements().first();
|
||||
if (!linked_report->conductors().isEmpty()) {
|
||||
c_list.append(linked_report->conductors().first());
|
||||
}
|
||||
if (known) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QSet<Conductor *> potential = seed->relatedPotentialConductors(true);
|
||||
potential.insert(seed);
|
||||
potentials.append(potential);
|
||||
c_list.append(seed);
|
||||
}
|
||||
|
||||
|
||||
return c_list;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ class ConductorCreator
|
||||
|
||||
private:
|
||||
static QList<Conductor *> existingPotential(const QList<Terminal *> &terminals_list);
|
||||
static ConductorProperties inheritedProperties(const Conductor *conductor);
|
||||
static QList<ConductorProperties> distinctProperties(const QList<Conductor *> &potentials);
|
||||
bool setUpPropertieToUse();
|
||||
Terminal *hubTerminal();
|
||||
QList<QPair<Terminal *, Terminal *>> terminalPairs(bool chain);
|
||||
|
||||
@@ -293,6 +293,11 @@ qet_add_app_test(tst_folioimageload tst_folioimageload.cpp)
|
||||
# selection has nothing to edit for a moment (#1439).
|
||||
qet_add_app_test(tst_selectionpanelheight tst_selectionpanelheight.cpp)
|
||||
|
||||
# Opening a project before the elements panel's first reload() left its tree
|
||||
# item detached, and selecting the item's first child crashed in QTreeModel
|
||||
# (pre-existing since a82f6de23).
|
||||
qet_add_app_test(tst_elementspanelprojectopen tst_elementspanelprojectopen.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.
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include "qetapptest.h"
|
||||
|
||||
#include "elementspanel.h"
|
||||
#include "projectview.h"
|
||||
#include "qetproject.h"
|
||||
|
||||
// projectWasOpened() adds the project to the elements panel. Before the panel's
|
||||
// first reload() the project's tree item is left detached (treeWidget() ==
|
||||
// nullptr), and selecting its first child crashed in QTreeModel
|
||||
// (pre-existing since a82f6de23).
|
||||
class tst_elementspanelprojectopen : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void addProjectBeforeReloadDoesNotCrash()
|
||||
{
|
||||
// A real one-folio project from the running app; diagrams()
|
||||
// builds the lazy folio, so the project item will have a child.
|
||||
QETDiagramEditor *editor = QetAppTest::editor();
|
||||
QVERIFY(editor);
|
||||
QVERIFY(editor->newProject());
|
||||
ProjectView *pv = editor->currentProjectView();
|
||||
QVERIFY(pv);
|
||||
QETProject *project = pv->project();
|
||||
QVERIFY(project);
|
||||
QVERIFY(!project->diagrams().isEmpty());
|
||||
|
||||
// Fresh panel: reload() has not run, so the project item is inserted detached.
|
||||
ElementsPanel panel;
|
||||
|
||||
// Without the guard this selects a detached item; with it, nothing is selected.
|
||||
panel.projectWasOpened(project);
|
||||
|
||||
QTreeWidgetItem *current = panel.currentItem();
|
||||
QVERIFY2(current == nullptr || current->treeWidget() == &panel,
|
||||
"the panel selected a detached item it does not own");
|
||||
}
|
||||
};
|
||||
|
||||
QET_APP_TEST_MAIN(tst_elementspanelprojectopen)
|
||||
|
||||
#include "tst_elementspanelprojectopen.moc"
|
||||
Reference in New Issue
Block a user