Merge remote-tracking branch 'upstream/master' into fix/keep-unloadable-items

This commit is contained in:
ispyisail
2026-10-19 20:21:02 +13:00
45 changed files with 54539 additions and 34741 deletions
+1599 -1026
View File
File diff suppressed because it is too large Load Diff
+1603 -1012
View File
File diff suppressed because it is too large Load Diff
+1600 -1024
View File
File diff suppressed because it is too large Load Diff
+1596 -1024
View File
File diff suppressed because it is too large Load Diff
+1561 -1025
View File
File diff suppressed because it is too large Load Diff
+1596 -1024
View File
File diff suppressed because it is too large Load Diff
+1603 -1012
View File
File diff suppressed because it is too large Load Diff
+1603 -1012
View File
File diff suppressed because it is too large Load Diff
+1562 -1019
View File
File diff suppressed because it is too large Load Diff
+1572 -1021
View File
File diff suppressed because it is too large Load Diff
+1570 -1019
View File
File diff suppressed because it is too large Load Diff
+1585 -1017
View File
File diff suppressed because it is too large Load Diff
+1604 -1013
View File
File diff suppressed because it is too large Load Diff
+1599 -1012
View File
File diff suppressed because it is too large Load Diff
+1570 -1023
View File
File diff suppressed because it is too large Load Diff
+1580 -1018
View File
File diff suppressed because it is too large Load Diff
+1589 -1017
View File
File diff suppressed because it is too large Load Diff
+1596 -1024
View File
File diff suppressed because it is too large Load Diff
+1596 -1024
View File
File diff suppressed because it is too large Load Diff
+1562 -1019
View File
File diff suppressed because it is too large Load Diff
+1604 -1024
View File
File diff suppressed because it is too large Load Diff
+1621 -1030
View File
File diff suppressed because it is too large Load Diff
+1593 -1017
View File
File diff suppressed because it is too large Load Diff
+1577 -1026
View File
File diff suppressed because it is too large Load Diff
+1565 -1018
View File
File diff suppressed because it is too large Load Diff
+1607 -1012
View File
File diff suppressed because it is too large Load Diff
+1678 -1059
View File
File diff suppressed because it is too large Load Diff
+1569 -1018
View File
File diff suppressed because it is too large Load Diff
+1565 -1018
View File
File diff suppressed because it is too large Load Diff
+1589 -1017
View File
File diff suppressed because it is too large Load Diff
+1585 -1017
View File
File diff suppressed because it is too large Load Diff
+1607 -1012
View File
File diff suppressed because it is too large Load Diff
+1599 -1012
View File
File diff suppressed because it is too large Load Diff
+1599 -1012
View File
File diff suppressed because it is too large Load Diff
+44 -8
View File
@@ -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 "
+80
View File
@@ -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)'}")
+21
View File
@@ -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()) {
+5 -3
View File
@@ -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
+22 -12
View File
@@ -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();
}
}
}
}
+106
View File
@@ -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()
{
+7
View File
@@ -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,
+99 -41
View File
@@ -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;
}
+2
View File
@@ -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);
+5
View File
@@ -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"